@scenography/plugin-artwork 0.2.0

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/dist/index.cjs ADDED
@@ -0,0 +1,358 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_data = require("./data-WcCPqQ_k.cjs");
3
+ let _scenography_engine = require("@scenography/engine");
4
+ let three = require("three");
5
+ //#region src/buildLabel.ts
6
+ const MM_PER_PT = .3528;
7
+ const UNITS_PER_MM = .1;
8
+ const PX_PER_UNIT = 48;
9
+ const ptToUnits = (pt) => pt * MM_PER_PT * UNITS_PER_MM;
10
+ const ptToPx = (pt) => ptToUnits(pt) * PX_PER_UNIT;
11
+ const TITLE_PT = 24;
12
+ const BODY_PT = 18;
13
+ const DOT_DIAMETER_UNITS = .5;
14
+ const PAD_PX = 4 * UNITS_PER_MM * PX_PER_UNIT;
15
+ const LINE_GAP_PX = 2 * UNITS_PER_MM * PX_PER_UNIT;
16
+ const AVAILABILITY_COLOR = {
17
+ "for-sale": "#22c55e",
18
+ sold: "#ef4444",
19
+ "not-for-sale": "#ef4444"
20
+ };
21
+ const has = (fields, field) => fields.includes(field);
22
+ const joinParts = (parts, sep) => parts.filter((p) => Boolean(p)).join(sep);
23
+ /**
24
+ * Build a physical wall label (cartela) for an artwork, rendered to a canvas
25
+ * texture at real physical size: a bold 24pt title, 18pt body, and a 5 mm
26
+ * availability dot in the top-right corner. Field visibility follows
27
+ * `artwork.label.fields` (default `DEFAULT_ARTWORK_FIELDS`); render order is fixed
28
+ * regardless of array order. Returns `null` when nothing is visible.
29
+ */
30
+ function buildLabel(artwork) {
31
+ const fields = artwork.label?.fields ?? require_data.DEFAULT_ARTWORK_FIELDS;
32
+ const lines = [];
33
+ if (has(fields, "title") && artwork.title) lines.push({
34
+ text: artwork.title,
35
+ px: ptToPx(TITLE_PT),
36
+ bold: true
37
+ });
38
+ const authorYear = joinParts([has(fields, "author") ? artwork.author : void 0, has(fields, "year") ? artwork.year?.toString() : void 0], ", ");
39
+ if (authorYear) lines.push({
40
+ text: authorYear,
41
+ px: ptToPx(BODY_PT),
42
+ bold: false
43
+ });
44
+ const techDims = joinParts([has(fields, "technique") ? artwork.technique : void 0, has(fields, "dimensions") ? artwork.dimensions : void 0], ", ");
45
+ if (techDims) lines.push({
46
+ text: techDims,
47
+ px: ptToPx(BODY_PT),
48
+ bold: false
49
+ });
50
+ const showDot = has(fields, "availability") && artwork.availability !== void 0;
51
+ if (lines.length === 0 && !showDot) return null;
52
+ const canvas = document.createElement("canvas");
53
+ const ctx = canvas.getContext("2d");
54
+ if (!ctx) return null;
55
+ const font = (line) => `${line.bold ? "700" : "400"} ${line.px}px system-ui, sans-serif`;
56
+ const dotPx = DOT_DIAMETER_UNITS * PX_PER_UNIT;
57
+ let maxTextWidth = 0;
58
+ for (const line of lines) {
59
+ ctx.font = font(line);
60
+ maxTextWidth = Math.max(maxTextWidth, ctx.measureText(line.text).width);
61
+ }
62
+ canvas.width = Math.ceil(maxTextWidth + (showDot ? 33.6 : 0) + 2 * PAD_PX);
63
+ const contentHeight = lines.reduce((sum, l) => sum + l.px + LINE_GAP_PX, 0);
64
+ canvas.height = Math.ceil(Math.max(contentHeight, dotPx) + 2 * PAD_PX);
65
+ ctx.fillStyle = "#f7f5f0";
66
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
67
+ ctx.textBaseline = "top";
68
+ ctx.fillStyle = "#1c1917";
69
+ let y = PAD_PX;
70
+ for (const line of lines) {
71
+ ctx.font = font(line);
72
+ ctx.fillText(line.text, PAD_PX, y);
73
+ y += line.px + LINE_GAP_PX;
74
+ }
75
+ if (showDot) {
76
+ ctx.beginPath();
77
+ ctx.arc(canvas.width - PAD_PX - dotPx / 2, 31.200000000000003, dotPx / 2, 0, Math.PI * 2);
78
+ ctx.fillStyle = AVAILABILITY_COLOR[artwork.availability];
79
+ ctx.fill();
80
+ }
81
+ const texture = new three.CanvasTexture(canvas);
82
+ texture.colorSpace = three.SRGBColorSpace;
83
+ texture.minFilter = three.LinearFilter;
84
+ texture.magFilter = three.LinearFilter;
85
+ const scale = artwork.label?.scale ?? 1;
86
+ const worldWidth = canvas.width / PX_PER_UNIT * scale;
87
+ const worldHeight = canvas.height / PX_PER_UNIT * scale;
88
+ const mesh = new three.Mesh(new three.PlaneGeometry(worldWidth, worldHeight), new three.MeshBasicMaterial({
89
+ map: texture,
90
+ transparent: true
91
+ }));
92
+ mesh.name = `label:${artwork.id}`;
93
+ return {
94
+ object: mesh,
95
+ worldWidth,
96
+ worldHeight
97
+ };
98
+ }
99
+ //#endregion
100
+ //#region src/placement.ts
101
+ const SURFACE_GAP = 1;
102
+ const DEFAULT_DEPTH$1 = 2;
103
+ const clamp01 = (t) => Math.min(Math.max(t, 0), 1);
104
+ const parseMeasure = (m) => typeof m === "number" ? {
105
+ value: m,
106
+ percent: false
107
+ } : {
108
+ value: parseFloat(m),
109
+ percent: true
110
+ };
111
+ /**
112
+ * Resolve an artwork's placement from its location, the resolved wall edges and
113
+ * the default eye height. Pure geometry — no three.js. Returns `null` for a
114
+ * wall piece whose edge is not (yet) in the index, so the caller can skip it.
115
+ */
116
+ function placeArtwork(artwork, edges, eyeHeight) {
117
+ const loc = artwork.location;
118
+ if (loc.type === "floating") return {
119
+ position: [
120
+ loc.point[0],
121
+ loc.point[1],
122
+ loc.point[2]
123
+ ],
124
+ yaw: loc.yaw ?? 0
125
+ };
126
+ const edge = edges.get(loc.edge);
127
+ if (!edge) return null;
128
+ const [ax, az] = edge.a;
129
+ const [bx, bz] = edge.b;
130
+ const dx = bx - ax;
131
+ const dz = bz - az;
132
+ const length = Math.hypot(dx, dz) || 1;
133
+ const ux = dx / length;
134
+ const uz = dz / length;
135
+ const pos = parseMeasure(loc.position);
136
+ const t = clamp01(pos.percent ? pos.value / 100 : pos.value / length);
137
+ const px = ax + dx * t;
138
+ const pz = az + dz * t;
139
+ let py = eyeHeight;
140
+ if (loc.height !== void 0) {
141
+ const h = parseMeasure(loc.height);
142
+ py = h.percent ? h.value / 100 * edge.height : h.value;
143
+ }
144
+ const sign = loc.side === "left" ? -1 : 1;
145
+ const nx = uz * sign;
146
+ const nz = -ux * sign;
147
+ const depth = artwork.depth ?? DEFAULT_DEPTH$1;
148
+ const offset = edge.thickness / 2 + SURFACE_GAP + depth / 2;
149
+ return {
150
+ position: [
151
+ px + nx * offset,
152
+ py,
153
+ pz + nz * offset
154
+ ],
155
+ yaw: Math.atan2(nx, nz)
156
+ };
157
+ }
158
+ //#endregion
159
+ //#region src/buildArtwork.ts
160
+ const loader = new three.TextureLoader();
161
+ const DEFAULT_DEPTH = 2;
162
+ const DEFAULT_FRAME_WIDTH = 5;
163
+ const DEFAULT_FRAME_DEPTH = 3;
164
+ const DEFAULT_FRAME_COLOR = "#2e2622";
165
+ const DEFAULT_MAT_WIDTH = 6;
166
+ const DEFAULT_MAT_COLOR = "#f4f0e8";
167
+ const MAT_GAP = .2;
168
+ const IMAGE_GAP = .4;
169
+ const frameMaterial = (frame) => {
170
+ const material = new three.MeshStandardMaterial({
171
+ color: frame.color ?? DEFAULT_FRAME_COLOR,
172
+ roughness: .7
173
+ });
174
+ return {
175
+ material,
176
+ needsUv1: frame.texture ? (0, _scenography_engine.applyTexture)(material, frame.texture) : false
177
+ };
178
+ };
179
+ /** Four flat bars forming a border around the front face, standing proud of it. */
180
+ function buildFrameBars(frame, width, height, front) {
181
+ const fw = frame.width ?? DEFAULT_FRAME_WIDTH;
182
+ const fd = frame.depth ?? DEFAULT_FRAME_DEPTH;
183
+ const { material, needsUv1 } = frameMaterial(frame);
184
+ const group = new three.Group();
185
+ const z = front + fd / 2;
186
+ const bars = [
187
+ [
188
+ width + 2 * fw,
189
+ fw,
190
+ 0,
191
+ height / 2 + fw / 2
192
+ ],
193
+ [
194
+ width + 2 * fw,
195
+ fw,
196
+ 0,
197
+ -(height / 2 + fw / 2)
198
+ ],
199
+ [
200
+ fw,
201
+ height,
202
+ -(width / 2 + fw / 2),
203
+ 0
204
+ ],
205
+ [
206
+ fw,
207
+ height,
208
+ width / 2 + fw / 2,
209
+ 0
210
+ ]
211
+ ];
212
+ for (const [bw, bh, x, y] of bars) {
213
+ const geometry = new three.BoxGeometry(bw, bh, fd);
214
+ if (needsUv1) {
215
+ const uv = geometry.getAttribute("uv");
216
+ if (uv) geometry.setAttribute("uv1", uv);
217
+ }
218
+ const bar = new three.Mesh(geometry, material);
219
+ bar.position.set(x, y, z);
220
+ bar.castShadow = true;
221
+ bar.receiveShadow = true;
222
+ group.add(bar);
223
+ }
224
+ return group;
225
+ }
226
+ /** Load an ornate frame from an external glTF/GLB, placed at the front face. */
227
+ function loadFrameModel(group, src, front) {
228
+ import("three/examples/jsm/loaders/GLTFLoader.js").then(({ GLTFLoader }) => {
229
+ new GLTFLoader().load(src, (gltf) => {
230
+ gltf.scene.position.z = front;
231
+ gltf.scene.traverse((child) => {
232
+ child.castShadow = true;
233
+ child.receiveShadow = true;
234
+ });
235
+ group.add(gltf.scene);
236
+ });
237
+ });
238
+ }
239
+ /**
240
+ * Build the artwork piece: a box (the image on the front, white sides and back),
241
+ * an optional passepartout mat, and an optional frame (generated bars or an
242
+ * external model). Shared by every location type.
243
+ */
244
+ function buildPiece(artwork) {
245
+ const [width, height] = artwork.size;
246
+ const depth = artwork.depth ?? DEFAULT_DEPTH;
247
+ const front = depth / 2;
248
+ const group = new three.Group();
249
+ const body = new three.Mesh(new three.BoxGeometry(width, height, depth), new three.MeshStandardMaterial({
250
+ color: "#ffffff",
251
+ roughness: .9
252
+ }));
253
+ body.castShadow = true;
254
+ body.receiveShadow = true;
255
+ group.add(body);
256
+ const frame = artwork.frame;
257
+ const mat = frame?.passepartout;
258
+ let imageWidth = width;
259
+ let imageHeight = height;
260
+ if (mat) {
261
+ const mw = mat.width ?? DEFAULT_MAT_WIDTH;
262
+ const matMesh = new three.Mesh(new three.PlaneGeometry(width, height), new three.MeshStandardMaterial({
263
+ color: mat.color ?? DEFAULT_MAT_COLOR,
264
+ roughness: .95
265
+ }));
266
+ matMesh.position.z = front + MAT_GAP;
267
+ matMesh.receiveShadow = true;
268
+ group.add(matMesh);
269
+ imageWidth = Math.max(width - 2 * mw, 1);
270
+ imageHeight = Math.max(height - 2 * mw, 1);
271
+ }
272
+ const texture = loader.load(artwork.src);
273
+ texture.colorSpace = three.SRGBColorSpace;
274
+ const image = new three.Mesh(new three.PlaneGeometry(imageWidth, imageHeight), new three.MeshStandardMaterial({
275
+ map: texture,
276
+ roughness: .9
277
+ }));
278
+ image.position.z = front + IMAGE_GAP;
279
+ image.receiveShadow = true;
280
+ group.add(image);
281
+ if (frame) if (frame.model) loadFrameModel(group, frame.model, front);
282
+ else group.add(buildFrameBars(frame, width, height, front));
283
+ return group;
284
+ }
285
+ /**
286
+ * Build the three.js object for an `Artwork`, placing it according to its
287
+ * `location`. Wall-hung pieces are positioned along a resolved edge (the box's
288
+ * back resting on the wall); floating pieces use explicit coordinates. Returns
289
+ * `null` if a wall location references an edge that no longer exists.
290
+ */
291
+ function buildArtwork(artwork, edges, eyeHeight) {
292
+ const group = buildPiece(artwork);
293
+ const depth = artwork.depth ?? DEFAULT_DEPTH;
294
+ const [pieceWidth, pieceHeight] = artwork.size;
295
+ const front = depth / 2;
296
+ const label = artwork.label ? buildLabel(artwork) : null;
297
+ if (label) {
298
+ const z = -depth / 2 + .5;
299
+ const side = artwork.label?.side ?? "below";
300
+ const frameExtent = artwork.frame ? artwork.frame.width ?? DEFAULT_FRAME_WIDTH : 0;
301
+ const [ox, oy] = artwork.label?.offset ?? [0, 0];
302
+ if (side === "below") {
303
+ const y = -(pieceHeight / 2 + frameExtent + 3 + label.worldHeight / 2);
304
+ label.object.position.set(ox, y + oy, z);
305
+ } else {
306
+ const x = (side === "left" ? -1 : 1) * (pieceWidth / 2 + frameExtent + 3 + label.worldWidth / 2);
307
+ label.object.position.set(x + ox, oy, z);
308
+ }
309
+ label.object.userData.pickTarget = artwork.id;
310
+ label.object.userData.pickInfo = {
311
+ width: label.worldWidth,
312
+ height: label.worldHeight,
313
+ frontOffset: .2
314
+ };
315
+ group.add(label.object);
316
+ } else {
317
+ group.userData.pickTarget = artwork.id;
318
+ group.userData.pickInfo = {
319
+ width: pieceWidth,
320
+ height: pieceHeight,
321
+ frontOffset: front + .6
322
+ };
323
+ }
324
+ const placement = placeArtwork(artwork, edges, eyeHeight);
325
+ if (!placement) return null;
326
+ group.position.set(...placement.position);
327
+ group.rotation.y = placement.yaw;
328
+ group.name = `artwork:${artwork.id}`;
329
+ group.userData.scenographyId = artwork.id;
330
+ group.userData.size = artwork.size;
331
+ return group;
332
+ }
333
+ //#endregion
334
+ //#region src/plugin.ts
335
+ const EMPTY_EDGES = /* @__PURE__ */ new Map();
336
+ const DEFAULT_EYE_HEIGHT = 150;
337
+ /**
338
+ * The artwork plugin: materializes each `Artwork` as a framed box (with an
339
+ * optional physical label), placed on a wall edge or floating. Consumes the
340
+ * `edges` capability (from the wall plugin) to resolve wall placements and
341
+ * `eyeHeight` for the default hang height, rebuilding when either changes.
342
+ */
343
+ const artwork = () => ({
344
+ data: require_data.artworkData,
345
+ dependsOn: [_scenography_engine.EDGES, _scenography_engine.EYE_HEIGHT],
346
+ build: (definition, ctx) => {
347
+ const object = buildArtwork(definition, ctx.capabilities.get(_scenography_engine.EDGES) ?? EMPTY_EDGES, ctx.capabilities.get(_scenography_engine.EYE_HEIGHT) ?? DEFAULT_EYE_HEIGHT);
348
+ return object ? { object } : {};
349
+ }
350
+ });
351
+ //#endregion
352
+ exports.DEFAULT_ARTWORK_FIELDS = require_data.DEFAULT_ARTWORK_FIELDS;
353
+ exports.artwork = artwork;
354
+ exports.artworkData = require_data.artworkData;
355
+ exports.buildArtwork = buildArtwork;
356
+ exports.validateArtwork = require_data.validateArtwork;
357
+
358
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["DEFAULT_ARTWORK_FIELDS","CanvasTexture","SRGBColorSpace","LinearFilter","Mesh","PlaneGeometry","MeshBasicMaterial","DEFAULT_DEPTH","TextureLoader","MeshStandardMaterial","Group","BoxGeometry","Mesh","PlaneGeometry","SRGBColorSpace","artworkData","EDGES","EYE_HEIGHT"],"sources":["../src/buildLabel.ts","../src/placement.ts","../src/buildArtwork.ts","../src/plugin.ts"],"sourcesContent":["import {\n CanvasTexture,\n LinearFilter,\n Mesh,\n MeshBasicMaterial,\n PlaneGeometry,\n SRGBColorSpace,\n type Object3D,\n} from \"three\";\nimport { DEFAULT_ARTWORK_FIELDS, type Artwork, type ArtworkField } from \"./data\";\n\n// World units are centimeters (100 units = 1 m), so the label is sized to real\n// physical dimensions. Text is rasterized to a canvas (zero assets shipped); the\n// canvas resolution (px per world unit) only affects crispness, not size.\nconst MM_PER_PT = 0.3528;\nconst UNITS_PER_MM = 0.1; // 1 unit = 1 cm = 10 mm\nconst PX_PER_UNIT = 48; // canvas resolution\n\nconst ptToUnits = (pt: number): number => pt * MM_PER_PT * UNITS_PER_MM;\nconst ptToPx = (pt: number): number => ptToUnits(pt) * PX_PER_UNIT;\n\nconst TITLE_PT = 24;\nconst BODY_PT = 18;\nconst DOT_DIAMETER_UNITS = 0.5; // 5 mm\nconst PAD_PX = 4 * UNITS_PER_MM * PX_PER_UNIT; // ~4 mm padding\nconst LINE_GAP_PX = 2 * UNITS_PER_MM * PX_PER_UNIT; // ~2 mm between lines\n\n/** Gap between the piece and its label, in world units (cm). */\nexport const LABEL_GAP = 3;\n\nconst AVAILABILITY_COLOR: Record<NonNullable<Artwork[\"availability\"]>, string> = {\n \"for-sale\": \"#22c55e\",\n sold: \"#ef4444\",\n \"not-for-sale\": \"#ef4444\",\n};\n\nconst has = (fields: readonly ArtworkField[], field: ArtworkField): boolean =>\n fields.includes(field);\nconst joinParts = (parts: (string | undefined)[], sep: string): string =>\n parts.filter((p): p is string => Boolean(p)).join(sep);\n\n/** A built physical label plus its real-world size (for placement/picking). */\nexport interface BuiltLabel {\n object: Object3D;\n worldWidth: number;\n worldHeight: number;\n}\n\n/**\n * Build a physical wall label (cartela) for an artwork, rendered to a canvas\n * texture at real physical size: a bold 24pt title, 18pt body, and a 5 mm\n * availability dot in the top-right corner. Field visibility follows\n * `artwork.label.fields` (default `DEFAULT_ARTWORK_FIELDS`); render order is fixed\n * regardless of array order. Returns `null` when nothing is visible.\n */\nexport function buildLabel(artwork: Artwork): BuiltLabel | null {\n const fields = artwork.label?.fields ?? DEFAULT_ARTWORK_FIELDS;\n\n const lines: { text: string; px: number; bold: boolean }[] = [];\n if (has(fields, \"title\") && artwork.title)\n lines.push({ text: artwork.title, px: ptToPx(TITLE_PT), bold: true });\n const authorYear = joinParts(\n [\n has(fields, \"author\") ? artwork.author : undefined,\n has(fields, \"year\") ? artwork.year?.toString() : undefined,\n ],\n \", \",\n );\n if (authorYear) lines.push({ text: authorYear, px: ptToPx(BODY_PT), bold: false });\n const techDims = joinParts(\n [\n has(fields, \"technique\") ? artwork.technique : undefined,\n has(fields, \"dimensions\") ? artwork.dimensions : undefined,\n ],\n \", \",\n );\n if (techDims) lines.push({ text: techDims, px: ptToPx(BODY_PT), bold: false });\n\n const showDot = has(fields, \"availability\") && artwork.availability !== undefined;\n if (lines.length === 0 && !showDot) return null;\n\n const canvas = document.createElement(\"canvas\");\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n const font = (line: { px: number; bold: boolean }): string =>\n `${line.bold ? \"700\" : \"400\"} ${line.px}px system-ui, sans-serif`;\n\n // Width from the widest line (plus room for the dot on the first line), height\n // from the stacked lines — so the card is exactly as big as its content.\n const dotPx = DOT_DIAMETER_UNITS * PX_PER_UNIT;\n let maxTextWidth = 0;\n for (const line of lines) {\n ctx.font = font(line);\n maxTextWidth = Math.max(maxTextWidth, ctx.measureText(line.text).width);\n }\n const dotReserve = showDot ? dotPx + LINE_GAP_PX : 0;\n canvas.width = Math.ceil(maxTextWidth + dotReserve + 2 * PAD_PX);\n const contentHeight = lines.reduce((sum, l) => sum + l.px + LINE_GAP_PX, 0);\n canvas.height = Math.ceil(Math.max(contentHeight, dotPx) + 2 * PAD_PX);\n\n ctx.fillStyle = \"#f7f5f0\";\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n ctx.textBaseline = \"top\";\n ctx.fillStyle = \"#1c1917\";\n let y = PAD_PX;\n for (const line of lines) {\n ctx.font = font(line);\n ctx.fillText(line.text, PAD_PX, y);\n y += line.px + LINE_GAP_PX;\n }\n\n if (showDot) {\n ctx.beginPath();\n ctx.arc(canvas.width - PAD_PX - dotPx / 2, PAD_PX + dotPx / 2, dotPx / 2, 0, Math.PI * 2);\n ctx.fillStyle = AVAILABILITY_COLOR[artwork.availability as NonNullable<Artwork[\"availability\"]>];\n ctx.fill();\n }\n\n const texture = new CanvasTexture(canvas);\n texture.colorSpace = SRGBColorSpace;\n texture.minFilter = LinearFilter;\n texture.magFilter = LinearFilter;\n\n // Real physical size, optionally scaled up for legibility (the canvas\n // resolution is unchanged, so the text just renders larger on the plane).\n const scale = artwork.label?.scale ?? 1;\n const worldWidth = (canvas.width / PX_PER_UNIT) * scale;\n const worldHeight = (canvas.height / PX_PER_UNIT) * scale;\n const mesh = new Mesh(\n new PlaneGeometry(worldWidth, worldHeight),\n new MeshBasicMaterial({ map: texture, transparent: true }),\n );\n mesh.name = `label:${artwork.id}`;\n return { object: mesh, worldWidth, worldHeight };\n}\n","import type { EdgeIndex } from \"@scenography/engine\";\nimport type { Measure } from \"@scenography/scene\";\n\nimport type { Artwork } from \"./data\";\n\nconst SURFACE_GAP = 1;\nconst DEFAULT_DEPTH = 2;\n\nconst clamp01 = (t: number): number => Math.min(Math.max(t, 0), 1);\n\nconst parseMeasure = (m: Measure): { value: number; percent: boolean } =>\n typeof m === \"number\" ? { value: m, percent: false } : { value: parseFloat(m), percent: true };\n\n/** Where an artwork sits: world position `[x, y, z]` and Y-axis rotation. */\nexport interface Placement {\n position: [number, number, number];\n yaw: number;\n}\n\n/**\n * Resolve an artwork's placement from its location, the resolved wall edges and\n * the default eye height. Pure geometry — no three.js. Returns `null` for a\n * wall piece whose edge is not (yet) in the index, so the caller can skip it.\n */\nexport function placeArtwork(\n artwork: Artwork,\n edges: EdgeIndex,\n eyeHeight: number,\n): Placement | null {\n const loc = artwork.location;\n if (loc.type === \"floating\") {\n return { position: [loc.point[0], loc.point[1], loc.point[2]], yaw: loc.yaw ?? 0 };\n }\n\n const edge = edges.get(loc.edge);\n if (!edge) return null;\n\n const [ax, az] = edge.a;\n const [bx, bz] = edge.b;\n const dx = bx - ax;\n const dz = bz - az;\n const length = Math.hypot(dx, dz) || 1;\n const ux = dx / length;\n const uz = dz / length;\n\n // Position along the edge.\n const pos = parseMeasure(loc.position);\n const t = clamp01(pos.percent ? pos.value / 100 : pos.value / length);\n const px = ax + dx * t;\n const pz = az + dz * t;\n\n // Vertical placement: eye height by default, or an explicit absolute/percent.\n let py = eyeHeight;\n if (loc.height !== undefined) {\n const h = parseMeasure(loc.height);\n py = h.percent ? (h.value / 100) * edge.height : h.value;\n }\n\n // Outward normal: right-hand side of the edge by default. Offset by the box\n // half-depth so its back rests against the wall surface.\n const sign = loc.side === \"left\" ? -1 : 1;\n const nx = uz * sign;\n const nz = -ux * sign;\n const depth = artwork.depth ?? DEFAULT_DEPTH;\n const offset = edge.thickness / 2 + SURFACE_GAP + depth / 2;\n\n return {\n position: [px + nx * offset, py, pz + nz * offset],\n yaw: Math.atan2(nx, nz),\n };\n}\n","import {\n BoxGeometry,\n Group,\n Mesh,\n MeshStandardMaterial,\n PlaneGeometry,\n SRGBColorSpace,\n TextureLoader,\n type Object3D,\n} from \"three\";\nimport { applyTexture, type EdgeIndex } from \"@scenography/engine\";\n\nimport type { Artwork, Frame } from \"./data\";\nimport { buildLabel, LABEL_GAP } from \"./buildLabel\";\nimport { placeArtwork } from \"./placement\";\n\nconst loader = new TextureLoader();\n\nconst DEFAULT_DEPTH = 2;\nconst DEFAULT_FRAME_WIDTH = 5;\nconst DEFAULT_FRAME_DEPTH = 3;\nconst DEFAULT_FRAME_COLOR = \"#2e2622\";\nconst DEFAULT_MAT_WIDTH = 6;\nconst DEFAULT_MAT_COLOR = \"#f4f0e8\";\n\n// Tiny z-offsets stacked on the box front to avoid z-fighting.\nconst MAT_GAP = 0.2;\nconst IMAGE_GAP = 0.4;\n\nconst frameMaterial = (frame: Frame): { material: MeshStandardMaterial; needsUv1: boolean } => {\n const material = new MeshStandardMaterial({\n color: frame.color ?? DEFAULT_FRAME_COLOR,\n roughness: 0.7,\n });\n const needsUv1 = frame.texture ? applyTexture(material, frame.texture) : false;\n return { material, needsUv1 };\n};\n\n/** Four flat bars forming a border around the front face, standing proud of it. */\nfunction buildFrameBars(frame: Frame, width: number, height: number, front: number): Group {\n const fw = frame.width ?? DEFAULT_FRAME_WIDTH;\n const fd = frame.depth ?? DEFAULT_FRAME_DEPTH;\n const { material, needsUv1 } = frameMaterial(frame);\n const group = new Group();\n const z = front + fd / 2;\n\n // Horizontals span the full outer width (covering the corners); verticals fill\n // the gap between them (butt joints).\n const bars: [number, number, number, number][] = [\n [width + 2 * fw, fw, 0, height / 2 + fw / 2], // top\n [width + 2 * fw, fw, 0, -(height / 2 + fw / 2)], // bottom\n [fw, height, -(width / 2 + fw / 2), 0], // left\n [fw, height, width / 2 + fw / 2, 0], // right\n ];\n for (const [bw, bh, x, y] of bars) {\n const geometry = new BoxGeometry(bw, bh, fd);\n if (needsUv1) {\n const uv = geometry.getAttribute(\"uv\");\n if (uv) geometry.setAttribute(\"uv1\", uv);\n }\n const bar = new Mesh(geometry, material);\n bar.position.set(x, y, z);\n bar.castShadow = true;\n bar.receiveShadow = true;\n group.add(bar);\n }\n return group;\n}\n\n/** Load an ornate frame from an external glTF/GLB, placed at the front face. */\nfunction loadFrameModel(group: Group, src: string, front: number): void {\n void import(\"three/examples/jsm/loaders/GLTFLoader.js\").then(({ GLTFLoader }) => {\n new GLTFLoader().load(src, (gltf) => {\n gltf.scene.position.z = front;\n gltf.scene.traverse((child) => {\n child.castShadow = true;\n child.receiveShadow = true;\n });\n group.add(gltf.scene);\n });\n });\n}\n\n/**\n * Build the artwork piece: a box (the image on the front, white sides and back),\n * an optional passepartout mat, and an optional frame (generated bars or an\n * external model). Shared by every location type.\n */\nfunction buildPiece(artwork: Artwork): Group {\n const [width, height] = artwork.size;\n const depth = artwork.depth ?? DEFAULT_DEPTH;\n const front = depth / 2;\n const group = new Group();\n\n const body = new Mesh(\n new BoxGeometry(width, height, depth),\n new MeshStandardMaterial({ color: \"#ffffff\", roughness: 0.9 }),\n );\n body.castShadow = true;\n body.receiveShadow = true;\n group.add(body);\n\n // Passepartout only applies when there's a frame.\n const frame = artwork.frame;\n const mat = frame?.passepartout;\n let imageWidth = width;\n let imageHeight = height;\n if (mat) {\n const mw = mat.width ?? DEFAULT_MAT_WIDTH;\n const matMesh = new Mesh(\n new PlaneGeometry(width, height),\n new MeshStandardMaterial({ color: mat.color ?? DEFAULT_MAT_COLOR, roughness: 0.95 }),\n );\n matMesh.position.z = front + MAT_GAP;\n matMesh.receiveShadow = true;\n group.add(matMesh);\n imageWidth = Math.max(width - 2 * mw, 1);\n imageHeight = Math.max(height - 2 * mw, 1);\n }\n\n const texture = loader.load(artwork.src);\n texture.colorSpace = SRGBColorSpace;\n const image = new Mesh(\n new PlaneGeometry(imageWidth, imageHeight),\n new MeshStandardMaterial({ map: texture, roughness: 0.9 }),\n );\n image.position.z = front + IMAGE_GAP;\n image.receiveShadow = true;\n group.add(image);\n\n if (frame) {\n if (frame.model) loadFrameModel(group, frame.model, front);\n else group.add(buildFrameBars(frame, width, height, front));\n }\n\n return group;\n}\n\n/**\n * Build the three.js object for an `Artwork`, placing it according to its\n * `location`. Wall-hung pieces are positioned along a resolved edge (the box's\n * back resting on the wall); floating pieces use explicit coordinates. Returns\n * `null` if a wall location references an edge that no longer exists.\n */\nexport function buildArtwork(\n artwork: Artwork,\n edges: EdgeIndex,\n eyeHeight: number,\n): Object3D | null {\n const group = buildPiece(artwork);\n const depth = artwork.depth ?? DEFAULT_DEPTH;\n const [pieceWidth, pieceHeight] = artwork.size;\n const front = depth / 2;\n\n // Physical wall label (cartela) next to the piece, when declared. Placed near\n // the wall surface (the box's back), not at the protruding front face. When a\n // label exists it's the click target; otherwise the piece itself is (set below).\n const label = artwork.label ? buildLabel(artwork) : null;\n if (label) {\n const z = -depth / 2 + 0.5;\n const side = artwork.label?.side ?? \"below\";\n // Clear the frame so the cartela isn't tucked under the (proud) frame bars.\n const frameExtent = artwork.frame ? (artwork.frame.width ?? DEFAULT_FRAME_WIDTH) : 0;\n const [ox, oy] = artwork.label?.offset ?? [0, 0];\n if (side === \"below\") {\n const y = -(pieceHeight / 2 + frameExtent + LABEL_GAP + label.worldHeight / 2);\n label.object.position.set(ox, y + oy, z);\n } else {\n const sign = side === \"left\" ? -1 : 1;\n const x = sign * (pieceWidth / 2 + frameExtent + LABEL_GAP + label.worldWidth / 2);\n label.object.position.set(x + ox, oy, z);\n }\n // The label mesh is the pick target; mark only it (not the group) so the\n // piece stays non-interactive when a label is present.\n label.object.userData.pickTarget = artwork.id;\n label.object.userData.pickInfo = { width: label.worldWidth, height: label.worldHeight, frontOffset: 0.2 };\n group.add(label.object);\n } else {\n // No label: the piece is the selector. Mark the group so any of its meshes\n // resolve to this id, and outline the front image face on hover.\n group.userData.pickTarget = artwork.id;\n group.userData.pickInfo = { width: pieceWidth, height: pieceHeight, frontOffset: front + 0.6 };\n }\n\n const placement = placeArtwork(artwork, edges, eyeHeight);\n if (!placement) return null;\n group.position.set(...placement.position);\n group.rotation.y = placement.yaw;\n\n group.name = `artwork:${artwork.id}`;\n group.userData.scenographyId = artwork.id;\n // Real-world face size, consumed by the camera framing to fit the piece.\n group.userData.size = artwork.size;\n return group;\n}\n","import { EDGES, EYE_HEIGHT, type EdgeIndex, type EnginePlugin } from \"@scenography/engine\";\n\nimport { buildArtwork } from \"./buildArtwork\";\nimport { artworkData, type Artwork } from \"./data\";\n\nconst EMPTY_EDGES: EdgeIndex = new Map();\nconst DEFAULT_EYE_HEIGHT = 150;\n\n/**\n * The artwork plugin: materializes each `Artwork` as a framed box (with an\n * optional physical label), placed on a wall edge or floating. Consumes the\n * `edges` capability (from the wall plugin) to resolve wall placements and\n * `eyeHeight` for the default hang height, rebuilding when either changes.\n */\nexport const artwork = (): EnginePlugin<Artwork> => ({\n data: artworkData,\n dependsOn: [EDGES, EYE_HEIGHT],\n build: (definition, ctx) => {\n const edges = ctx.capabilities.get<EdgeIndex>(EDGES) ?? EMPTY_EDGES;\n const eyeHeight = ctx.capabilities.get<number>(EYE_HEIGHT) ?? DEFAULT_EYE_HEIGHT;\n const object = buildArtwork(definition, edges, eyeHeight);\n return object ? { object } : {};\n },\n});\n"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { a as ArtworkField, c as Availability, d as Passepartout, i as Artwork, l as DEFAULT_ARTWORK_FIELDS, n as validateArtwork, o as ArtworkLabel, s as ArtworkLocation, t as artworkData, u as Frame } from "./index-XUkegdM8.cjs";
2
+ import { EdgeIndex, EnginePlugin } from "@scenography/engine";
3
+ import { Object3D } from "three";
4
+
5
+ //#region src/plugin.d.ts
6
+ /**
7
+ * The artwork plugin: materializes each `Artwork` as a framed box (with an
8
+ * optional physical label), placed on a wall edge or floating. Consumes the
9
+ * `edges` capability (from the wall plugin) to resolve wall placements and
10
+ * `eyeHeight` for the default hang height, rebuilding when either changes.
11
+ */
12
+ declare const artwork: () => EnginePlugin<Artwork>;
13
+ //#endregion
14
+ //#region src/buildArtwork.d.ts
15
+ /**
16
+ * Build the three.js object for an `Artwork`, placing it according to its
17
+ * `location`. Wall-hung pieces are positioned along a resolved edge (the box's
18
+ * back resting on the wall); floating pieces use explicit coordinates. Returns
19
+ * `null` if a wall location references an edge that no longer exists.
20
+ */
21
+ declare function buildArtwork(artwork: Artwork, edges: EdgeIndex, eyeHeight: number): Object3D | null;
22
+ //#endregion
23
+ export { type Artwork, type ArtworkField, type ArtworkLabel, type ArtworkLocation, type Availability, DEFAULT_ARTWORK_FIELDS, type Frame, type Passepartout, artwork, artworkData, buildArtwork, validateArtwork };
24
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,24 @@
1
+ import { a as ArtworkField, c as Availability, d as Passepartout, i as Artwork, l as DEFAULT_ARTWORK_FIELDS, n as validateArtwork, o as ArtworkLabel, s as ArtworkLocation, t as artworkData, u as Frame } from "./index-XUkegdM8.mjs";
2
+ import { EdgeIndex, EnginePlugin } from "@scenography/engine";
3
+ import { Object3D } from "three";
4
+
5
+ //#region src/plugin.d.ts
6
+ /**
7
+ * The artwork plugin: materializes each `Artwork` as a framed box (with an
8
+ * optional physical label), placed on a wall edge or floating. Consumes the
9
+ * `edges` capability (from the wall plugin) to resolve wall placements and
10
+ * `eyeHeight` for the default hang height, rebuilding when either changes.
11
+ */
12
+ declare const artwork: () => EnginePlugin<Artwork>;
13
+ //#endregion
14
+ //#region src/buildArtwork.d.ts
15
+ /**
16
+ * Build the three.js object for an `Artwork`, placing it according to its
17
+ * `location`. Wall-hung pieces are positioned along a resolved edge (the box's
18
+ * back resting on the wall); floating pieces use explicit coordinates. Returns
19
+ * `null` if a wall location references an edge that no longer exists.
20
+ */
21
+ declare function buildArtwork(artwork: Artwork, edges: EdgeIndex, eyeHeight: number): Object3D | null;
22
+ //#endregion
23
+ export { type Artwork, type ArtworkField, type ArtworkLabel, type ArtworkLocation, type Availability, DEFAULT_ARTWORK_FIELDS, type Frame, type Passepartout, artwork, artworkData, buildArtwork, validateArtwork };
24
+ //# sourceMappingURL=index.d.mts.map