partforge 0.134.0 → 0.136.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/docs/AUTHORING-PARTS.md +45 -14
- package/package.json +1 -1
- package/src/app-materials.js +3 -1
- package/src/framework/fonts.js +8 -0
- package/src/framework/jobs.js +38 -11
- package/src/framework/lint/rules-materials.js +9 -2
- package/src/framework/materials/assets/SOURCES.md +10 -4
- package/src/framework/materials/assets/pattern-plywood-color.jpg +0 -0
- package/src/framework/materials/assets/pattern-plywood-normal.jpg +0 -0
- package/src/framework/materials/assets/pattern-plywood-rough.jpg +0 -0
- package/src/framework/materials/assets.js +3 -0
- package/src/framework/materials/patterns.js +103 -0
- package/src/framework/materials/physical.js +5 -2
- package/src/framework/materials/presets.js +11 -5
- package/src/framework/materials/resolve.js +4 -2
- package/src/framework/materials/sheet-look.js +204 -0
- package/src/framework/mount.js +61 -32
- package/src/framework/panel/font-picker.js +76 -5
- package/src/framework/panel/widgets/font.js +13 -1
- package/src/framework/sheet/part.js +32 -17
- package/src/framework/vectors.js +3 -2
- package/src/framework/viewer.js +53 -29
- package/src/parts/laser-box.js +1 -1
- package/src/parts/material-swatches.js +58 -2
- package/types/part.d.ts +4 -0
package/src/framework/viewer.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ensureBoxUVs } from "./materials/uv.js";
|
|
|
12
12
|
import { loadEnvironmentRig } from "./materials/environment.js";
|
|
13
13
|
import { assetUrl } from "./materials/assets.js";
|
|
14
14
|
import { resolveEnvironmentId } from "./materials/resolve.js";
|
|
15
|
+
import { burnsFor, realisticDisplay } from "./materials/sheet-look.js";
|
|
15
16
|
import { neutralToneMapToSrgb8 } from "./materials/tonemap-readback.js";
|
|
16
17
|
import { createCutaway } from "./cutaway.js";
|
|
17
18
|
import { CUTAWAY_OVERLAY_RENDER_ORDER } from "./cutaway-render.js";
|
|
@@ -591,9 +592,11 @@ export function createViewer(container, part) {
|
|
|
591
592
|
const textureCache = new Map(); // asset file name -> Texture
|
|
592
593
|
const textureLoader = new THREE.TextureLoader();
|
|
593
594
|
let pmrem = null;
|
|
594
|
-
let printFrames = {};
|
|
595
|
-
let
|
|
596
|
-
let
|
|
595
|
+
let printFrames = {}; // name -> mat4 array: layer lines' display → export map
|
|
596
|
+
let sheetFrames = {}; // name -> { frame, t, plies }: a burning sheet's canonical frame (sheet-look.js)
|
|
597
|
+
let frameSource = null; // () => { print, sheet }; mount's lazy provider (setFrameSource)
|
|
598
|
+
let framesStale = false; // the source has frames the materials have not seen
|
|
599
|
+
const IDENTITY_MAT4 = new THREE.Matrix4().toArray();
|
|
597
600
|
const modeListeners = new Set();
|
|
598
601
|
const envListeners = new Set();
|
|
599
602
|
const SHADOW_LOWRES_MS = 100; // at most one low-res shadow per this, while a part moves
|
|
@@ -728,9 +731,14 @@ export function createViewer(container, part) {
|
|
|
728
731
|
function physicalFor(name) {
|
|
729
732
|
let m = physicalMats.get(name);
|
|
730
733
|
if (!m) {
|
|
731
|
-
|
|
734
|
+
const sp = part.parts[name];
|
|
735
|
+
// `burn` is decided once per sub-part (sheet-look.js burnsFor): only a laser sheet in
|
|
736
|
+
// a wood compiles the burn pass, so every other program is exactly what it was.
|
|
737
|
+
// realisticDisplay: a laser sheet with no material draws as its stock (sheet-look.js).
|
|
738
|
+
m = cloneKeepsPattern(buildPhysicalMaterial(realisticDisplay(sp), { printFrame: printFrames[name], burn: burnsFor(sp), loadTexture }));
|
|
732
739
|
physicalMats.set(name, m);
|
|
733
740
|
syncGrain(name);
|
|
741
|
+
applySheetFrame(m, sheetFrames[name]);
|
|
734
742
|
}
|
|
735
743
|
return m;
|
|
736
744
|
}
|
|
@@ -859,7 +867,7 @@ export function createViewer(container, part) {
|
|
|
859
867
|
// ground only moves on showAssembly, never because a capture happened.
|
|
860
868
|
function enterRealistic(rig, { live = true, reground = true } = {}) {
|
|
861
869
|
try {
|
|
862
|
-
|
|
870
|
+
syncFrames(); // before the swap: the first realistic frame has the export-pose layers and the sheets' burns
|
|
863
871
|
renderMode = "realistic";
|
|
864
872
|
// A regen can land between the proxy compile and here, while the mode
|
|
865
873
|
// was still CAD, so setSubGeometry skipped the UVs. Idempotent.
|
|
@@ -929,7 +937,7 @@ export function createViewer(container, part) {
|
|
|
929
937
|
// target bound — each only for the synchronous compile() inside
|
|
930
938
|
// compileAsync, so the view on screen is untouched while it waits.
|
|
931
939
|
function compileRealistic(rig, { forCapture = false } = {}) {
|
|
932
|
-
|
|
940
|
+
syncFrames();
|
|
933
941
|
const proxy = new THREE.Scene();
|
|
934
942
|
proxy.environment = rig.envMap;
|
|
935
943
|
for (const n of names) {
|
|
@@ -1030,30 +1038,45 @@ export function createViewer(container, part) {
|
|
|
1030
1038
|
for (const [n, m] of physicalMats) m.userData.patternUniforms?.pfPrintFrame.value.fromArray(printFrames[n] ?? identity);
|
|
1031
1039
|
}
|
|
1032
1040
|
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1041
|
+
// A burning sheet's canonical frame (sheet-look.js sheetFrameFor). No frame — not yet
|
|
1042
|
+
// delivered, or not computable at these params — is pfSheetT = 0: plain wood.
|
|
1043
|
+
function applySheetFrame(m, f) {
|
|
1044
|
+
const u = m.userData.patternUniforms;
|
|
1045
|
+
if (!u?.pfSheetFrame) return;
|
|
1046
|
+
u.pfSheetFrame.value.fromArray(f?.frame ?? IDENTITY_MAT4);
|
|
1047
|
+
u.pfSheetT.value = f?.t ?? 0;
|
|
1048
|
+
u.pfPlies.value = f?.plies ?? 0;
|
|
1049
|
+
}
|
|
1050
|
+
function setSheetFrames(frames) {
|
|
1051
|
+
sheetFrames = frames ?? {};
|
|
1052
|
+
for (const [n, m] of physicalMats) applySheetFrame(m, sheetFrames[n]);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// The lazy form mount uses. Frames are PULLED only when something is about to draw the
|
|
1056
|
+
// realistic look: the live view (or a switch loading), and any capture that borrows it —
|
|
1057
|
+
// compileRealistic and enterRealistic are the two doors every such path goes through. A
|
|
1058
|
+
// delivery just marks them stale (invalidateFrames); in CAD nothing is computed. One
|
|
1059
|
+
// source carries both kinds: `print` (layer lines; a placed sub-part's costs two
|
|
1060
|
+
// geometry-free probe builds, and every sub-part with no material draws layer lines) and
|
|
1061
|
+
// `sheet` (a burning sheet's canonical frame, from data).
|
|
1062
|
+
function setFrameSource(source) {
|
|
1063
|
+
frameSource = typeof source === "function" ? source : null;
|
|
1064
|
+
invalidateFrames();
|
|
1065
|
+
}
|
|
1066
|
+
function invalidateFrames() {
|
|
1067
|
+
framesStale = true;
|
|
1068
|
+
if (renderMode === "realistic" || realisticPending) syncFrames();
|
|
1069
|
+
}
|
|
1070
|
+
function syncFrames() {
|
|
1071
|
+
if (!frameSource || !framesStale) return;
|
|
1072
|
+
framesStale = false;
|
|
1051
1073
|
let frames;
|
|
1052
|
-
try { frames =
|
|
1053
|
-
console.warn("partforge: computing
|
|
1074
|
+
try { frames = frameSource(); } catch (e) {
|
|
1075
|
+
console.warn("partforge: computing material frames failed", e);
|
|
1054
1076
|
return;
|
|
1055
1077
|
}
|
|
1056
|
-
setPrintFrames(frames);
|
|
1078
|
+
setPrintFrames(frames?.print);
|
|
1079
|
+
setSheetFrames(frames?.sheet);
|
|
1057
1080
|
}
|
|
1058
1081
|
|
|
1059
1082
|
// Resolves once the current environment's rig is loaded (captures wait on it).
|
|
@@ -2735,8 +2758,9 @@ export function createViewer(container, part) {
|
|
|
2735
2758
|
isRealisticPending: () => realisticPending,
|
|
2736
2759
|
onEnvironmentChange: (cb) => { envListeners.add(cb); return () => envListeners.delete(cb); },
|
|
2737
2760
|
setPrintFrames,
|
|
2738
|
-
|
|
2739
|
-
|
|
2761
|
+
setSheetFrames,
|
|
2762
|
+
setFrameSource,
|
|
2763
|
+
invalidateFrames,
|
|
2740
2764
|
whenRealisticReady,
|
|
2741
2765
|
dispose,
|
|
2742
2766
|
}, () => requestRender(API_GRACE_MS));
|
package/src/parts/laser-box.js
CHANGED
|
@@ -42,7 +42,7 @@ const hingeSlots = (k, p, d, pose, z) => d.hingeX.flatMap((hx) =>
|
|
|
42
42
|
[hx - HINGE.tongueX, hx + HINGE.tongueX].map((x) =>
|
|
43
43
|
k.shape2d(tab(p).slot).translate(worldToSheet(pose, [x, p.depth / 2, z]))));
|
|
44
44
|
|
|
45
|
-
const PLY = { views: ["box"], display: { material: "
|
|
45
|
+
const PLY = { views: ["box"], display: { material: "plywood" }, material: "birch plywood", thickness: (p) => p.t };
|
|
46
46
|
const panel = (name, label, extra = {}) => sheetPart({
|
|
47
47
|
...PLY, label,
|
|
48
48
|
profile: (k, p, d) => d.box[name].outline, // drawn as seen from outside
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Dev-only contact sheet for the material library: one 30 mm sample per preset,
|
|
2
2
|
// laid out in a grid, so a change to a preset or the pattern shader can be
|
|
3
|
-
// judged by eye in every environment (materials.html).
|
|
3
|
+
// judged by eye in every environment (materials.html). The "Laser-cut" views
|
|
4
|
+
// (below) do the same for the laser burn. Not a reference part.
|
|
4
5
|
import { PRESETS } from "../framework/materials/presets.js";
|
|
6
|
+
import { sheetPart } from "../framework/sheet/part.js";
|
|
7
|
+
import { sheetHole } from "../framework/sheet/joinery.js";
|
|
5
8
|
|
|
6
9
|
const ids = Object.keys(PRESETS);
|
|
7
10
|
const COLS = 6, PITCH = 40;
|
|
@@ -19,10 +22,59 @@ const FILAMENT_COLOURS = [
|
|
|
19
22
|
const withHole = (k) =>
|
|
20
23
|
k.box({ size: [30, 30, 12] }).fillet({ r: 3 }).cut(k.cylinder({ r: 5, h: 20 }).translate([0, 0, -4]));
|
|
21
24
|
|
|
25
|
+
// The "Laser-cut" view: sheet swatches as the laser burn draws them — each 30 × 30 with a
|
|
26
|
+
// cut hole, an engraved label (its thickness) and a score line — in every wood at 3, 6 and
|
|
27
|
+
// 9 mm; one oak panel standing on edge (posed, so its frame is not the identity); and the
|
|
28
|
+
// looks that must NOT burn: a clear-acrylic sheet and an oak block that is not a sheet part.
|
|
29
|
+
// "Laser-cut, unburnt" is its twin: the same swatches with an identity author `place`,
|
|
30
|
+
// which shows plain wood (materials/sheet-look.js) — same geometry, same place, same
|
|
31
|
+
// material — so the two views differ ONLY by the burn and scripts/capture-contact-sheet.mjs
|
|
32
|
+
// can diff them pixel for pixel — and one row of sheets that name no material, drawn as
|
|
33
|
+
// their stock (sheet-look.js STOCK_LOOKS).
|
|
34
|
+
const SW = 30;
|
|
35
|
+
const WOODS = ["plywood", "oak", "walnut"];
|
|
36
|
+
const CONTROLS_Y = -WOODS.length * PITCH;
|
|
37
|
+
const LASER_SWATCHES = [
|
|
38
|
+
...WOODS.flatMap((material, row) => [3, 6, 9].map((t, col) => ({
|
|
39
|
+
key: `${material}_${t}`, label: `${PRESETS[material].label}, ${t} mm`, at: [col * PITCH, -row * PITCH], t, display: { material },
|
|
40
|
+
}))),
|
|
41
|
+
{ key: "acrylic_3", label: "Clear acrylic, 3 mm", at: [0, CONTROLS_Y], t: 3, display: { material: "clear-acrylic" }, stock: "clear acrylic" },
|
|
42
|
+
{ key: "oak_standing", label: "Oak, 3 mm, standing", at: [PITCH, CONTROLS_Y], t: 3, display: { material: "oak" }, standing: true },
|
|
43
|
+
{ key: "oak_block", label: "Oak block (not a sheet part)", at: [2 * PITCH, CONTROLS_Y], t: 3, display: { material: "oak" }, block: true },
|
|
44
|
+
{ key: "stock_plywood", label: "No material, \"birch plywood\" stock", at: [0, CONTROLS_Y - PITCH], t: 3, stock: "birch plywood" },
|
|
45
|
+
{ key: "stock_acrylic", label: "No material, \"clear acrylic\" stock", at: [PITCH, CONTROLS_Y - PITCH], t: 3, stock: "clear acrylic" },
|
|
46
|
+
{ key: "stock_stained", label: "A bare colour on \"birch plywood\" stock", at: [2 * PITCH, CONTROLS_Y - PITCH], t: 3, stock: "birch plywood", display: { color: 0x2e8b3d } },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function laserSwatch(s, unburnt) {
|
|
50
|
+
const [x, y] = s.at;
|
|
51
|
+
const common = {
|
|
52
|
+
label: unburnt ? `${s.label} (unburnt)` : s.label,
|
|
53
|
+
views: [unburnt ? "unburnt" : "laser"],
|
|
54
|
+
...(s.display ? { display: s.display } : {}),
|
|
55
|
+
};
|
|
56
|
+
if (s.block) {
|
|
57
|
+
return { ...common, build: (k) => k.box({ min: [x, y, 0], max: [x + SW, y + SW, s.t] })
|
|
58
|
+
.cut(k.cylinder({ d: 8, h: s.t + 2 }).translate([x + 21, y + 21, -1])) };
|
|
59
|
+
}
|
|
60
|
+
// A standing swatch is drawn at the drawing origin; its pose stands it on edge at `at`.
|
|
61
|
+
const [u, v] = s.standing ? [0, 0] : [x, y];
|
|
62
|
+
return sheetPart({
|
|
63
|
+
...common,
|
|
64
|
+
material: s.stock ?? "birch plywood",
|
|
65
|
+
thickness: s.t,
|
|
66
|
+
profile: (k) => k.shape2d([[u, v], [u + SW, v], [u + SW, v + SW], [u, v + SW]]).cut(sheetHole({ d: 8, at: [u + 21, v + 21] })),
|
|
67
|
+
engrave: (k) => k.text2d(String(s.t), { size: 10, align: "center", valign: "middle" }).translate([u + 9, v + 20]),
|
|
68
|
+
score: () => [[[u + 3, v + 7], [u + SW - 3, v + 7]]],
|
|
69
|
+
...(s.standing ? { pose: { face: "-Y", up: "+Z", at: [x, y, 0] } } : {}),
|
|
70
|
+
...(unburnt ? { place: (solid) => solid } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
22
74
|
export default {
|
|
23
75
|
meta: { title: "Material swatches", units: "mm", environment: "studio" },
|
|
24
76
|
defaults: {},
|
|
25
|
-
views: { all: { label: "All presets", default: true }, colours: { label: "PLA colours" }, petg: { label: "PETG colours" }, print: { label: "Layer lines" } },
|
|
77
|
+
views: { all: { label: "All presets", default: true }, colours: { label: "PLA colours" }, petg: { label: "PETG colours" }, print: { label: "Layer lines" }, laser: { label: "Laser-cut" }, unburnt: { label: "Laser-cut, unburnt" } },
|
|
26
78
|
parts: {
|
|
27
79
|
...Object.fromEntries(ids.map((id, i) => [id.replaceAll("-", "_"), {
|
|
28
80
|
label: PRESETS[id].label,
|
|
@@ -65,5 +117,9 @@ export default {
|
|
|
65
117
|
build: (k) => k.box({ size: [30, 12, 40] }).translate([50, 0, 0]),
|
|
66
118
|
display: { material: "pla-print", color: 0xe0592a },
|
|
67
119
|
},
|
|
120
|
+
...Object.fromEntries(LASER_SWATCHES.flatMap((s) => [
|
|
121
|
+
[`laser_${s.key}`, laserSwatch(s, false)],
|
|
122
|
+
[`unburnt_${s.key}`, laserSwatch(s, true)],
|
|
123
|
+
])),
|
|
68
124
|
},
|
|
69
125
|
};
|
package/types/part.d.ts
CHANGED
|
@@ -441,6 +441,10 @@ export interface SheetDeclaration<P = ResolvedParams, D = Derived> {
|
|
|
441
441
|
pose: SheetPose | ((p: P, d: D) => SheetPose | null) | null;
|
|
442
442
|
/** The build `sheetPart` made; `sub.build !== sub.sheet.generatedBuild` is a custom build. */
|
|
443
443
|
generatedBuild: (k: GeometryKernel, p: P, d: D) => Solid;
|
|
444
|
+
/** The author's own `place` (the spec's), run after the pose; `null` when there is none. */
|
|
445
|
+
place: ((solid: Solid, ctx: PlaceContext<P, D>) => Solid) | null;
|
|
446
|
+
/** The `place` `sheetPart` installed (the pose, then the author's own), or `null`; `sub.place !== sub.sheet.generatedPlace` is a replaced place. */
|
|
447
|
+
generatedPlace: ((solid: Solid, ctx: PlaceContext<P, D>) => Solid) | null;
|
|
444
448
|
}
|
|
445
449
|
|
|
446
450
|
export interface ViewDefinition {
|