@vessel-dsp/stompbox 0.6.4 → 0.6.6
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.d.ts +37 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +255 -48
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +8 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +28 -0
- package/dist/node.js.map +1 -0
- package/package.json +58 -53
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
1
|
import { defaultControlState, extractPanel, parseCircuitDocumentFile, } from "@vessel-dsp/core";
|
|
3
2
|
export function getAvailableStompboxStyleProfiles(profiles, filter) {
|
|
4
3
|
return profiles.filter((profile) => profile.supportedKnobCounts.includes(filter.knobCount));
|
|
@@ -171,31 +170,8 @@ export function validateStompboxGlbAsset(bytes, partProfile, options = {}) {
|
|
|
171
170
|
diagnostics,
|
|
172
171
|
};
|
|
173
172
|
}
|
|
174
|
-
export function validateStompboxGlbAssetFile(path, partProfile) {
|
|
175
|
-
try {
|
|
176
|
-
return validateStompboxGlbAsset(new Uint8Array(readFileSync(path)), partProfile, {
|
|
177
|
-
assetPath: path,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
-
const diagnostic = {
|
|
183
|
-
code: "invalid-glb-asset",
|
|
184
|
-
message: `Invalid GLB asset for stompbox part "${partProfile.id}": ${message}`,
|
|
185
|
-
partId: partProfile.id,
|
|
186
|
-
assetPath: path,
|
|
187
|
-
};
|
|
188
|
-
return {
|
|
189
|
-
schema: "stompbox-glb-asset-validation/v1",
|
|
190
|
-
partProfileId: partProfile.id,
|
|
191
|
-
assetPath: path,
|
|
192
|
-
valid: false,
|
|
193
|
-
targets: {},
|
|
194
|
-
diagnostics: [diagnostic],
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
173
|
export function validateStompboxHardwareProfileAssets(hardwareProfile, options = {}) {
|
|
174
|
+
const readAssetFile = requireStompboxAssetFileReader(options);
|
|
199
175
|
const partIds = options.partIds ?? defaultLiveStatePartProfileIds(hardwareProfile);
|
|
200
176
|
const assets = {};
|
|
201
177
|
const diagnostics = [];
|
|
@@ -211,7 +187,7 @@ export function validateStompboxHardwareProfileAssets(hardwareProfile, options =
|
|
|
211
187
|
continue;
|
|
212
188
|
}
|
|
213
189
|
const assetPath = resolveStompboxAssetPaths(partProfile.assets, options).glb;
|
|
214
|
-
const validation =
|
|
190
|
+
const validation = validateStompboxGlbAssetFromPath(assetPath, partProfile, readAssetFile);
|
|
215
191
|
assets[partId] = validation;
|
|
216
192
|
diagnostics.push(...validation.diagnostics);
|
|
217
193
|
}
|
|
@@ -222,6 +198,30 @@ export function validateStompboxHardwareProfileAssets(hardwareProfile, options =
|
|
|
222
198
|
diagnostics,
|
|
223
199
|
};
|
|
224
200
|
}
|
|
201
|
+
export function validateStompboxGlbAssetFromPath(path, partProfile, readAssetFile) {
|
|
202
|
+
try {
|
|
203
|
+
return validateStompboxGlbAsset(readAssetFile(path), partProfile, {
|
|
204
|
+
assetPath: path,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
+
const diagnostic = {
|
|
210
|
+
code: "invalid-glb-asset",
|
|
211
|
+
message: `Invalid GLB asset for stompbox part "${partProfile.id}": ${message}`,
|
|
212
|
+
partId: partProfile.id,
|
|
213
|
+
assetPath: path,
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
schema: "stompbox-glb-asset-validation/v1",
|
|
217
|
+
partProfileId: partProfile.id,
|
|
218
|
+
assetPath: path,
|
|
219
|
+
valid: false,
|
|
220
|
+
targets: {},
|
|
221
|
+
diagnostics: [diagnostic],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
225
|
export function createStompboxSourcePanelControls(document) {
|
|
226
226
|
const componentsById = new Map(document.components.map((component) => [component.id, component]));
|
|
227
227
|
const controls = [];
|
|
@@ -232,12 +232,14 @@ export function createStompboxSourcePanelControls(document) {
|
|
|
232
232
|
element.kind !== "footswitch") {
|
|
233
233
|
continue;
|
|
234
234
|
}
|
|
235
|
+
const sourceControlId = controlIdForPanelElement(element);
|
|
235
236
|
const sourceComponentId = element.bind.componentId;
|
|
236
237
|
const component = sourceComponentId === undefined
|
|
237
238
|
? undefined
|
|
238
239
|
: componentsById.get(sourceComponentId);
|
|
239
240
|
const label = nonEmptyText(element.label) ??
|
|
240
241
|
nonEmptyText(component?.name) ??
|
|
242
|
+
nonEmptyText(sourceControlId) ??
|
|
241
243
|
nonEmptyText(sourceComponentId) ??
|
|
242
244
|
nonEmptyText(element.id) ??
|
|
243
245
|
"Control";
|
|
@@ -246,7 +248,7 @@ export function createStompboxSourcePanelControls(document) {
|
|
|
246
248
|
const options = sourcePanelControlOptions(component);
|
|
247
249
|
const description = sourcePanelControlPropertyText(component, "Description");
|
|
248
250
|
controls.push({
|
|
249
|
-
id:
|
|
251
|
+
id: sourceControlId ??
|
|
250
252
|
element.id ??
|
|
251
253
|
`${face.id}-${element.grid.row}-${element.grid.column}`,
|
|
252
254
|
label,
|
|
@@ -266,9 +268,7 @@ export function createStompboxControlSurface(document, options) {
|
|
|
266
268
|
const diagnostics = [];
|
|
267
269
|
const basePanel = extractPanel(document);
|
|
268
270
|
const panelControls = createStompboxSourcePanelControls(document);
|
|
269
|
-
const sourceControls = panelControls
|
|
270
|
-
? sourceControlsFromExtractedPanel(basePanel)
|
|
271
|
-
: panelControls;
|
|
271
|
+
const sourceControls = mergeSourcePanelControls(panelControls, sourceControlsFromExtractedPanel(basePanel));
|
|
272
272
|
const unmatchedCompiled = [...(options.compiledControls ?? [])];
|
|
273
273
|
const descriptors = [];
|
|
274
274
|
for (const sourceControl of sourceControls) {
|
|
@@ -568,7 +568,7 @@ export function createStompboxDrillLayout(document, options = {}) {
|
|
|
568
568
|
const panelDeclared = [...declared, ...gridDeclared];
|
|
569
569
|
const declaredControlIds = new Set(panelDeclared.flatMap((candidate) => candidate.controlId === undefined ? [] : [candidate.controlId]));
|
|
570
570
|
const auto = autoPlacementCandidates(panel, enclosure, panelDeclared, declaredControlIds, options, hardwareProfile, placementStyle, diagnostics);
|
|
571
|
-
const holes = [...panelDeclared, ...auto].flatMap((candidate) => drillHoleForCandidate(candidate, hardwareProfile, diagnostics));
|
|
571
|
+
const holes = collapseConcentricMounts([...panelDeclared, ...auto], hardwareProfile, diagnostics).flatMap((candidate) => drillHoleForCandidate(candidate, hardwareProfile, diagnostics));
|
|
572
572
|
diagnostics.push(...validateHolePlacements(holes, enclosure, options.minPartClearanceMm));
|
|
573
573
|
return {
|
|
574
574
|
schema: "stompbox-drill-layout/v1",
|
|
@@ -593,7 +593,27 @@ export function createStompboxPreview(document, options = {}) {
|
|
|
593
593
|
const resolveOptions = assetResolveOptions(options);
|
|
594
594
|
const runtimeState = options.pedalState?.controls ?? options.state;
|
|
595
595
|
const enabled = options.pedalState?.enabled;
|
|
596
|
-
const parts = drillLayout.holes.
|
|
596
|
+
const parts = drillLayout.holes.flatMap((hole) => {
|
|
597
|
+
const base = previewPartForHole(hole, drillLayout.enclosure, controlMetadata.get(hole.controlId ?? ""), runtimeState, resolveOptions, options.appearance, enabled);
|
|
598
|
+
const dials = hole.concentricDials ?? [];
|
|
599
|
+
if (dials.length === 0) {
|
|
600
|
+
return [base];
|
|
601
|
+
}
|
|
602
|
+
const stacked = dials.map((dial) => {
|
|
603
|
+
const dialPart = previewPartForHole(concentricDialHole(hole, dial), drillLayout.enclosure, controlMetadata.get(dial.controlId ?? ""), runtimeState, resolveOptions, options.appearance, enabled);
|
|
604
|
+
return {
|
|
605
|
+
...dialPart,
|
|
606
|
+
transform: {
|
|
607
|
+
...dialPart.transform,
|
|
608
|
+
translationMm: {
|
|
609
|
+
...dialPart.transform.translationMm,
|
|
610
|
+
z: dialPart.transform.translationMm.z + dial.stackOffsetMm,
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
return [base, ...stacked];
|
|
616
|
+
});
|
|
597
617
|
const decals = [
|
|
598
618
|
...normalizeDecals(options.decals, drillLayout.enclosure),
|
|
599
619
|
...controlLabelDecals(drillLayout, placementStyle, options.appearance),
|
|
@@ -825,6 +845,9 @@ export function createStompboxPreviewGlb(document, options = {}) {
|
|
|
825
845
|
? undefined
|
|
826
846
|
: validateStompboxHardwareProfileAssets(hardwareProfile, {
|
|
827
847
|
basePath: options.basePath,
|
|
848
|
+
...(options.readAssetFile === undefined
|
|
849
|
+
? {}
|
|
850
|
+
: { readAssetFile: options.readAssetFile }),
|
|
828
851
|
partIds: liveStatePartProfileIdsForPreview(preview),
|
|
829
852
|
});
|
|
830
853
|
const diagnostics = [
|
|
@@ -936,6 +959,19 @@ function sourceControlsFromExtractedPanel(panel) {
|
|
|
936
959
|
})),
|
|
937
960
|
];
|
|
938
961
|
}
|
|
962
|
+
function mergeSourcePanelControls(panelControls, extractedControls) {
|
|
963
|
+
if (panelControls.length === 0) {
|
|
964
|
+
return extractedControls;
|
|
965
|
+
}
|
|
966
|
+
const seenIds = new Set(panelControls.map((control) => control.id));
|
|
967
|
+
const seenComponents = new Set(panelControls.flatMap((control) => control.sourceComponentId === undefined ? [] : [control.sourceComponentId]));
|
|
968
|
+
return [
|
|
969
|
+
...panelControls,
|
|
970
|
+
...extractedControls.filter((control) => !seenIds.has(control.id) &&
|
|
971
|
+
(control.sourceComponentId === undefined ||
|
|
972
|
+
!seenComponents.has(control.sourceComponentId))),
|
|
973
|
+
];
|
|
974
|
+
}
|
|
939
975
|
function findMatchingCompiledControlIndex(sourceControl, compiledControls, diagnostics) {
|
|
940
976
|
if (sourceControl.sourceComponentId !== undefined) {
|
|
941
977
|
const byComponent = compiledControls.findIndex((control) => control.sourceComponentId === sourceControl.sourceComponentId);
|
|
@@ -971,9 +1007,8 @@ function runtimeControlDescriptor(sourceControl, compiledControl) {
|
|
|
971
1007
|
const rawValue = compiledControl === undefined
|
|
972
1008
|
? (sourceControl?.value ?? min)
|
|
973
1009
|
: effectiveCompiledControlValue(compiledControl);
|
|
974
|
-
const id = sourceControl?.
|
|
1010
|
+
const id = sourceControl?.id ??
|
|
975
1011
|
compiledControl?.sourceComponentId ??
|
|
976
|
-
sourceControl?.id ??
|
|
977
1012
|
publicRuntimeControlId(compiledControl?.id ?? compiledControl?.name ?? "control");
|
|
978
1013
|
const label = sourceControl?.label ?? compiledControl?.name ?? id;
|
|
979
1014
|
const normalizedValue = kind === "switch"
|
|
@@ -1412,6 +1447,12 @@ function requireStompboxHardwareProfile(options) {
|
|
|
1412
1447
|
}
|
|
1413
1448
|
return options.hardwareProfile;
|
|
1414
1449
|
}
|
|
1450
|
+
function requireStompboxAssetFileReader(options) {
|
|
1451
|
+
if (options.readAssetFile === undefined) {
|
|
1452
|
+
throw new Error("stompbox GLB asset file access requires options.readAssetFile or the @vessel-dsp/stompbox/node export");
|
|
1453
|
+
}
|
|
1454
|
+
return options.readAssetFile;
|
|
1455
|
+
}
|
|
1415
1456
|
function resolveStompboxPlacementStyle(profile, hardwareProfile) {
|
|
1416
1457
|
return {
|
|
1417
1458
|
defaultPartIds: {
|
|
@@ -1509,8 +1550,8 @@ function declaredPhysicalPlacements(faces, controls, hardwareProfile, defaultPar
|
|
|
1509
1550
|
const metadata = controls.get(controlId);
|
|
1510
1551
|
const requestedPartId = element.physical.partProfileId ??
|
|
1511
1552
|
defaultPartIdForPanelKind(element.kind, metadata, defaultPartIds);
|
|
1512
|
-
const
|
|
1513
|
-
if (
|
|
1553
|
+
const partResolution = knownPartIdOrDefault(requestedPartId, element.kind, metadata, hardwareProfile, defaultPartIds, diagnostics, controlId, element.id);
|
|
1554
|
+
if (partResolution === undefined) {
|
|
1514
1555
|
diagnostics.push({
|
|
1515
1556
|
code: "unsupported-control",
|
|
1516
1557
|
message: `Unsupported declared panel element kind "${element.kind}"`,
|
|
@@ -1526,7 +1567,10 @@ function declaredPhysicalPlacements(faces, controls, hardwareProfile, defaultPar
|
|
|
1526
1567
|
kind: element.kind,
|
|
1527
1568
|
face: face.id,
|
|
1528
1569
|
centerMm: pointFromCorePoint(element.physical.centerMm),
|
|
1529
|
-
partId,
|
|
1570
|
+
partId: partResolution.partId,
|
|
1571
|
+
...(partResolution.partProvenance === undefined
|
|
1572
|
+
? {}
|
|
1573
|
+
: { partProvenance: partResolution.partProvenance }),
|
|
1530
1574
|
componentId: element.bind.componentId,
|
|
1531
1575
|
controlId,
|
|
1532
1576
|
...(label === undefined ? {} : { label }),
|
|
@@ -1536,7 +1580,15 @@ function declaredPhysicalPlacements(faces, controls, hardwareProfile, defaultPar
|
|
|
1536
1580
|
...(element.physical.locked === undefined
|
|
1537
1581
|
? {}
|
|
1538
1582
|
: { locked: element.physical.locked }),
|
|
1539
|
-
|
|
1583
|
+
...(element.physical.mountId === undefined
|
|
1584
|
+
? {}
|
|
1585
|
+
: { mountId: element.physical.mountId }),
|
|
1586
|
+
...(element.physical.surface === undefined
|
|
1587
|
+
? {}
|
|
1588
|
+
: { surface: element.physical.surface }),
|
|
1589
|
+
provenance: partResolution.partProvenance === "defaulted"
|
|
1590
|
+
? "auto-generated"
|
|
1591
|
+
: "vdsp-declared",
|
|
1540
1592
|
});
|
|
1541
1593
|
}
|
|
1542
1594
|
}
|
|
@@ -1553,8 +1605,8 @@ function gridPhysicalPlacements(faces, controls, enclosure, hardwareProfile, def
|
|
|
1553
1605
|
const metadata = controls.get(controlId);
|
|
1554
1606
|
const requestedPartId = element.physical?.partProfileId ??
|
|
1555
1607
|
defaultPartIdForPanelKind(element.kind, metadata, defaultPartIds);
|
|
1556
|
-
const
|
|
1557
|
-
if (
|
|
1608
|
+
const partResolution = knownPartIdOrDefault(requestedPartId, element.kind, metadata, hardwareProfile, defaultPartIds, diagnostics, controlId, element.id);
|
|
1609
|
+
if (partResolution === undefined) {
|
|
1558
1610
|
diagnostics.push({
|
|
1559
1611
|
code: "unsupported-control",
|
|
1560
1612
|
message: `Unsupported panel grid element kind "${element.kind}"`,
|
|
@@ -1570,7 +1622,10 @@ function gridPhysicalPlacements(faces, controls, enclosure, hardwareProfile, def
|
|
|
1570
1622
|
kind: element.kind,
|
|
1571
1623
|
face: face.id,
|
|
1572
1624
|
centerMm: panelGridCenterMm(face, element, enclosure),
|
|
1573
|
-
partId,
|
|
1625
|
+
partId: partResolution.partId,
|
|
1626
|
+
...(partResolution.partProvenance === undefined
|
|
1627
|
+
? {}
|
|
1628
|
+
: { partProvenance: partResolution.partProvenance }),
|
|
1574
1629
|
componentId: element.bind.componentId,
|
|
1575
1630
|
controlId,
|
|
1576
1631
|
...(label === undefined ? {} : { label }),
|
|
@@ -1580,6 +1635,12 @@ function gridPhysicalPlacements(faces, controls, enclosure, hardwareProfile, def
|
|
|
1580
1635
|
...(element.physical?.locked === undefined
|
|
1581
1636
|
? {}
|
|
1582
1637
|
: { locked: element.physical.locked }),
|
|
1638
|
+
...(element.physical?.mountId === undefined
|
|
1639
|
+
? {}
|
|
1640
|
+
: { mountId: element.physical.mountId }),
|
|
1641
|
+
...(element.physical?.surface === undefined
|
|
1642
|
+
? {}
|
|
1643
|
+
: { surface: element.physical.surface }),
|
|
1583
1644
|
}, diagnostics));
|
|
1584
1645
|
}
|
|
1585
1646
|
}
|
|
@@ -1752,6 +1813,92 @@ function autoCandidate(candidate, diagnostics) {
|
|
|
1752
1813
|
provenance: "auto-generated",
|
|
1753
1814
|
};
|
|
1754
1815
|
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Collapses placement candidates that share a `mountId` onto a multi-surface
|
|
1818
|
+
* part into a single base candidate carrying the upper dials as
|
|
1819
|
+
* `concentricDials`. One mount becomes one drill hole with N stacked dials,
|
|
1820
|
+
* ordered by the part profile's `surfaces`. Candidates without a `mountId`, or
|
|
1821
|
+
* whose part declares no `surfaces`, pass through unchanged (one hole each).
|
|
1822
|
+
*/
|
|
1823
|
+
function collapseConcentricMounts(candidates, hardwareProfile, diagnostics) {
|
|
1824
|
+
const result = [];
|
|
1825
|
+
const groups = new Map();
|
|
1826
|
+
for (const candidate of candidates) {
|
|
1827
|
+
if (candidate.mountId === undefined) {
|
|
1828
|
+
result.push(candidate);
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
const members = groups.get(candidate.mountId) ?? [];
|
|
1832
|
+
members.push(candidate);
|
|
1833
|
+
groups.set(candidate.mountId, members);
|
|
1834
|
+
}
|
|
1835
|
+
for (const [mountId, members] of groups) {
|
|
1836
|
+
const part = hardwareProfile.partProfiles[members[0]?.partId ?? ""];
|
|
1837
|
+
const surfaces = part?.surfaces;
|
|
1838
|
+
if (part === undefined || surfaces === undefined || surfaces.length === 0) {
|
|
1839
|
+
// Not a multi-surface part: keep each member as its own hole.
|
|
1840
|
+
result.push(...members);
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
const memberBySurface = new Map();
|
|
1844
|
+
for (const member of members) {
|
|
1845
|
+
if (member.surface === undefined) {
|
|
1846
|
+
result.push(member);
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
if (!surfaces.some((surface) => surface.id === member.surface)) {
|
|
1850
|
+
diagnostics.push({
|
|
1851
|
+
code: "unknown-part-surface",
|
|
1852
|
+
message: `Mount "${mountId}" references surface "${member.surface}" not declared by part "${part.id}"`,
|
|
1853
|
+
...(member.controlId === undefined
|
|
1854
|
+
? {}
|
|
1855
|
+
: { controlId: member.controlId }),
|
|
1856
|
+
placementId: member.id,
|
|
1857
|
+
face: member.face,
|
|
1858
|
+
});
|
|
1859
|
+
result.push(member);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
memberBySurface.set(member.surface, member);
|
|
1863
|
+
}
|
|
1864
|
+
const missing = surfaces.filter((surface) => !memberBySurface.has(surface.id));
|
|
1865
|
+
if (missing.length > 0) {
|
|
1866
|
+
diagnostics.push({
|
|
1867
|
+
code: "concentric-mount-incomplete",
|
|
1868
|
+
message: `Concentric mount "${mountId}" (part "${part.id}") is missing surface(s): ${missing.map((surface) => surface.id).join(", ")}`,
|
|
1869
|
+
placementId: members[0]?.id ?? mountId,
|
|
1870
|
+
face: members[0]?.face ?? "top",
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
const ordered = [];
|
|
1874
|
+
for (const surface of surfaces) {
|
|
1875
|
+
const member = memberBySurface.get(surface.id);
|
|
1876
|
+
if (member !== undefined) {
|
|
1877
|
+
ordered.push({ surface, member });
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
const base = ordered[0];
|
|
1881
|
+
if (base === undefined) {
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
const upperDials = ordered
|
|
1885
|
+
.slice(1)
|
|
1886
|
+
.map(({ surface, member }) => ({
|
|
1887
|
+
surface: surface.id,
|
|
1888
|
+
partGeometry: surface.geometry,
|
|
1889
|
+
stackOffsetMm: surface.stackOffsetMm,
|
|
1890
|
+
...(member.controlId === undefined
|
|
1891
|
+
? {}
|
|
1892
|
+
: { controlId: member.controlId }),
|
|
1893
|
+
...(member.componentId === undefined
|
|
1894
|
+
? {}
|
|
1895
|
+
: { componentId: member.componentId }),
|
|
1896
|
+
...(member.label === undefined ? {} : { label: member.label }),
|
|
1897
|
+
}));
|
|
1898
|
+
result.push({ ...base.member, concentricDials: upperDials });
|
|
1899
|
+
}
|
|
1900
|
+
return result;
|
|
1901
|
+
}
|
|
1755
1902
|
function drillHoleForCandidate(candidate, hardwareProfile, diagnostics) {
|
|
1756
1903
|
const part = hardwareProfile.partProfiles[candidate.partId];
|
|
1757
1904
|
if (part === undefined) {
|
|
@@ -1779,6 +1926,9 @@ function drillHoleForCandidate(candidate, hardwareProfile, diagnostics) {
|
|
|
1779
1926
|
partLabel: part.label,
|
|
1780
1927
|
partFamily: part.family,
|
|
1781
1928
|
partGeometry: part.geometry,
|
|
1929
|
+
...(candidate.partProvenance === undefined
|
|
1930
|
+
? {}
|
|
1931
|
+
: { partProvenance: candidate.partProvenance }),
|
|
1782
1932
|
...(part.assetScale === undefined ? {} : { assetScale: part.assetScale }),
|
|
1783
1933
|
...(candidate.controlId === undefined
|
|
1784
1934
|
? {}
|
|
@@ -1793,6 +1943,10 @@ function drillHoleForCandidate(candidate, hardwareProfile, diagnostics) {
|
|
|
1793
1943
|
...(part.stateTargets === undefined
|
|
1794
1944
|
? {}
|
|
1795
1945
|
: { stateTargets: part.stateTargets }),
|
|
1946
|
+
...(candidate.concentricDials === undefined ||
|
|
1947
|
+
candidate.concentricDials.length === 0
|
|
1948
|
+
? {}
|
|
1949
|
+
: { concentricDials: candidate.concentricDials }),
|
|
1796
1950
|
},
|
|
1797
1951
|
];
|
|
1798
1952
|
}
|
|
@@ -1897,6 +2051,42 @@ function isOutOfBounds(hole, enclosure) {
|
|
|
1897
2051
|
}
|
|
1898
2052
|
return false;
|
|
1899
2053
|
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Builds a synthetic drill hole for one upper dial of a concentric mount so it
|
|
2056
|
+
* can be rendered as its own preview part. It reuses the base hole's position
|
|
2057
|
+
* but carries the dial's geometry and control; the caller applies the dial's
|
|
2058
|
+
* `stackOffsetMm` to lift it above the base dial.
|
|
2059
|
+
*/
|
|
2060
|
+
function concentricDialHole(hole, dial) {
|
|
2061
|
+
return {
|
|
2062
|
+
id: `${hole.id}-${dial.surface}`,
|
|
2063
|
+
face: hole.face,
|
|
2064
|
+
centerMm: hole.centerMm,
|
|
2065
|
+
drillDiameterMm: hole.drillDiameterMm,
|
|
2066
|
+
...(hole.drillHoleProfileId === undefined
|
|
2067
|
+
? {}
|
|
2068
|
+
: { drillHoleProfileId: hole.drillHoleProfileId }),
|
|
2069
|
+
partId: hole.partId,
|
|
2070
|
+
partLabel: hole.partLabel,
|
|
2071
|
+
partFamily: hole.partFamily,
|
|
2072
|
+
partGeometry: dial.partGeometry,
|
|
2073
|
+
...(hole.partProvenance === undefined
|
|
2074
|
+
? {}
|
|
2075
|
+
: { partProvenance: hole.partProvenance }),
|
|
2076
|
+
...(hole.assetScale === undefined ? {} : { assetScale: hole.assetScale }),
|
|
2077
|
+
...(dial.controlId === undefined ? {} : { controlId: dial.controlId }),
|
|
2078
|
+
...(dial.componentId === undefined
|
|
2079
|
+
? {}
|
|
2080
|
+
: { componentId: dial.componentId }),
|
|
2081
|
+
...(dial.label === undefined ? {} : { label: dial.label }),
|
|
2082
|
+
provenance: hole.provenance,
|
|
2083
|
+
...(hole.locked === undefined ? {} : { locked: hole.locked }),
|
|
2084
|
+
assets: hole.assets,
|
|
2085
|
+
...(hole.stateTargets === undefined
|
|
2086
|
+
? {}
|
|
2087
|
+
: { stateTargets: hole.stateTargets }),
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
1900
2090
|
function previewPartForHole(hole, enclosure, metadata, state, assetOptions, appearance, enabled) {
|
|
1901
2091
|
const rotation = baseRotationForFace(hole.face);
|
|
1902
2092
|
const stateValue = stateValueForHole(hole, state, enabled);
|
|
@@ -1922,6 +2112,9 @@ function previewPartForHole(hole, enclosure, metadata, state, assetOptions, appe
|
|
|
1922
2112
|
partId: hole.partId,
|
|
1923
2113
|
family: hole.partFamily,
|
|
1924
2114
|
geometry: hole.partGeometry,
|
|
2115
|
+
...(hole.partProvenance === undefined
|
|
2116
|
+
? {}
|
|
2117
|
+
: { partProvenance: hole.partProvenance }),
|
|
1925
2118
|
...(hole.assetScale === undefined ? {} : { assetScale: hole.assetScale }),
|
|
1926
2119
|
...(hole.controlId === undefined ? {} : { controlId: hole.controlId }),
|
|
1927
2120
|
face: hole.face,
|
|
@@ -3022,6 +3215,7 @@ function gltfAssemblySources(preview, options, assetValidation) {
|
|
|
3022
3215
|
throw new Error("stompbox GLB assembly requires options.basePath for caller-provided asset files");
|
|
3023
3216
|
}
|
|
3024
3217
|
const basePath = options.basePath;
|
|
3218
|
+
const readAssetFile = requireStompboxAssetFileReader(options);
|
|
3025
3219
|
return [
|
|
3026
3220
|
{
|
|
3027
3221
|
id: preview.enclosure.variantId,
|
|
@@ -3029,6 +3223,7 @@ function gltfAssemblySources(preview, options, assetValidation) {
|
|
|
3029
3223
|
displayGlb: preview.enclosure.assets.glb,
|
|
3030
3224
|
displayStep: preview.enclosure.assets.step,
|
|
3031
3225
|
localGlbPath: resolveStompboxAssetPaths(preview.drillLayout.enclosure.assets, { basePath }).glb,
|
|
3226
|
+
readAssetFile,
|
|
3032
3227
|
...(preview.enclosure.material === undefined
|
|
3033
3228
|
? {}
|
|
3034
3229
|
: { material: preview.enclosure.material }),
|
|
@@ -3051,10 +3246,10 @@ function gltfAssemblySources(preview, options, assetValidation) {
|
|
|
3051
3246
|
: { material: previewMaterialJson(preview.enclosure.material) }),
|
|
3052
3247
|
},
|
|
3053
3248
|
},
|
|
3054
|
-
...preview.parts.map((part) => partAssemblySource(part, preview.drillLayout, basePath, assetValidation?.assets[part.partId])),
|
|
3249
|
+
...preview.parts.map((part) => partAssemblySource(part, preview.drillLayout, basePath, readAssetFile, assetValidation?.assets[part.partId])),
|
|
3055
3250
|
];
|
|
3056
3251
|
}
|
|
3057
|
-
function partAssemblySource(part, layout, basePath, validation) {
|
|
3252
|
+
function partAssemblySource(part, layout, basePath, readAssetFile, validation) {
|
|
3058
3253
|
const sourceAssets = sourceAssetRefsForPreviewPart(layout, part);
|
|
3059
3254
|
const sourceMaterial = part.geometry.kind === "led-bezel" ? undefined : part.material;
|
|
3060
3255
|
const transform = {
|
|
@@ -3072,6 +3267,7 @@ function partAssemblySource(part, layout, basePath, validation) {
|
|
|
3072
3267
|
displayGlb: part.assets.glb,
|
|
3073
3268
|
displayStep: part.assets.step,
|
|
3074
3269
|
localGlbPath: resolveStompboxAssetPaths(sourceAssets, { basePath }).glb,
|
|
3270
|
+
readAssetFile,
|
|
3075
3271
|
...(sourceMaterial === undefined ? {} : { material: sourceMaterial }),
|
|
3076
3272
|
...(materialTargets.length === 0 ? {} : { materialTargets }),
|
|
3077
3273
|
...(stateTargets === undefined ? {} : { stateTargets }),
|
|
@@ -3082,6 +3278,9 @@ function partAssemblySource(part, layout, basePath, validation) {
|
|
|
3082
3278
|
partId: part.partId,
|
|
3083
3279
|
face: part.face,
|
|
3084
3280
|
provenance: part.provenance,
|
|
3281
|
+
...(part.partProvenance === undefined
|
|
3282
|
+
? {}
|
|
3283
|
+
: { partProvenance: part.partProvenance }),
|
|
3085
3284
|
glb: part.assets.glb,
|
|
3086
3285
|
step: part.assets.step,
|
|
3087
3286
|
...(part.assetScale === undefined ? {} : { assetScale: part.assetScale }),
|
|
@@ -3558,7 +3757,7 @@ function appendAssemblySource(state, source) {
|
|
|
3558
3757
|
return wrapperIndex;
|
|
3559
3758
|
}
|
|
3560
3759
|
function appendSourceGlb(state, source) {
|
|
3561
|
-
const parsed = parseGlbFile(source.localGlbPath);
|
|
3760
|
+
const parsed = parseGlbFile(source.localGlbPath, source.readAssetFile);
|
|
3562
3761
|
const bufferOffset = appendBinaryChunk(state, parsed.binary.slice(0, parsed.bufferByteLength));
|
|
3563
3762
|
const sourceMaterials = jsonObjectArray(parsed.json, "materials");
|
|
3564
3763
|
const bufferViewOffset = state.bufferViews.length;
|
|
@@ -3715,8 +3914,8 @@ function sourceSceneRootNodeIndexes(json) {
|
|
|
3715
3914
|
}
|
|
3716
3915
|
return nodes.flatMap((node) => (typeof node === "number" ? [node] : []));
|
|
3717
3916
|
}
|
|
3718
|
-
function parseGlbFile(path) {
|
|
3719
|
-
const bytes =
|
|
3917
|
+
function parseGlbFile(path, readAssetFile) {
|
|
3918
|
+
const bytes = readAssetFile(path);
|
|
3720
3919
|
return parseGlbBytes(bytes, path);
|
|
3721
3920
|
}
|
|
3722
3921
|
function parseGlbBytes(bytes, context) {
|
|
@@ -4412,7 +4611,7 @@ function defaultPartIdForPanelKind(kind, metadata, defaultPartIds) {
|
|
|
4412
4611
|
function knownPartIdOrDefault(requestedPartId, kind, metadata, hardwareProfile, defaultPartIds, diagnostics, controlId, placementId) {
|
|
4413
4612
|
if (requestedPartId !== undefined &&
|
|
4414
4613
|
hardwareProfile.partProfiles[requestedPartId] !== undefined) {
|
|
4415
|
-
return requestedPartId;
|
|
4614
|
+
return { partId: requestedPartId, partProvenance: "vdsp-declared" };
|
|
4416
4615
|
}
|
|
4417
4616
|
if (requestedPartId !== undefined) {
|
|
4418
4617
|
diagnostics.push({
|
|
@@ -4422,7 +4621,15 @@ function knownPartIdOrDefault(requestedPartId, kind, metadata, hardwareProfile,
|
|
|
4422
4621
|
...(placementId === undefined ? {} : { placementId }),
|
|
4423
4622
|
});
|
|
4424
4623
|
}
|
|
4425
|
-
|
|
4624
|
+
const defaultPartId = defaultPartIdForPanelKind(kind, metadata, defaultPartIds);
|
|
4625
|
+
return defaultPartId === undefined
|
|
4626
|
+
? undefined
|
|
4627
|
+
: {
|
|
4628
|
+
partId: defaultPartId,
|
|
4629
|
+
...(requestedPartId === undefined
|
|
4630
|
+
? {}
|
|
4631
|
+
: { partProvenance: "defaulted" }),
|
|
4632
|
+
};
|
|
4426
4633
|
}
|
|
4427
4634
|
function placementIdForKind(kind, controlId) {
|
|
4428
4635
|
if (kind === "footswitch") {
|