fix(shelf): stop top-face z-fighting + avoid plate/side gaps
Two follow-ups to the plate-recess fix: - Top surface: full-height frame members that pass UNDER the top board (back panel, bookshelf column dividers, corner posts) had their top face at unitHeight — coplanar with the top board's top — so they z-fought through it (the seam on the top). Drop their top 1mm (cappedFrameY); bottoms stay on the floor so nothing floats. - Plate width: the previous pass recessed every board's width, which opened a 1mm gap where boards ABUT the side panels (cubby / bookshelf-with-sides). Width is now recessed only where boards span OVER posts (open-rack / no-sides bookshelf); abutting boards keep full width and meet the sides flush. Depth stays recessed everywhere (gap-free, fixes the back-panel fight). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e17c298284
commit
b602e04b70
@@ -63,16 +63,33 @@ function stampShelfSlot(mesh: Mesh, slotId: ShelfSlotId): Mesh {
|
|||||||
return mesh
|
return mesh
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plates (boards) span the full footprint, so their end / front / back faces
|
// A board's front/back faces land on the frame's outer faces (posts / back panel)
|
||||||
// land exactly on the frame's (posts / sides / back) outer faces — coplanar
|
// — coplanar surfaces the depth buffer can't separate, which flickers as z-fighting.
|
||||||
// surfaces the depth buffer can't separate, which flickers as z-fighting. Recess
|
// Recess 1mm so the board sits just inside: the meshes still overlap (no gap), but
|
||||||
// every board 1mm on width + depth so it sits just inside the frame: the meshes
|
// no faces are coplanar. Depth is always recessed (boards reach into the back panel
|
||||||
// still overlap (no visible gap), but no two faces are coplanar.
|
// / posts). Width is recessed only at call sites where boards span OVER posts
|
||||||
|
// (open-rack / no-sides bookshelf); boards that ABUT side panels keep full width so
|
||||||
|
// they meet the sides flush — abutting faces are back-to-back and never fight.
|
||||||
const BOARD_INSET = 0.001
|
const BOARD_INSET = 0.001
|
||||||
|
|
||||||
function boardGeometry(width: number, thickness: number, depth: number): BoxGeometry {
|
// Frame members that pass under the top board (dividers / back / corner posts) reach
|
||||||
|
// y=unitHeight, coplanar with the top board's top face → z-fighting. Drop their top 1mm so
|
||||||
|
// the board cleanly caps them; their bottom stays on the floor.
|
||||||
|
const FRAME_TOP_INSET = 0.001
|
||||||
|
|
||||||
|
function cappedFrameY(unitHeight: number): { height: number; centerY: number } {
|
||||||
|
const height = Math.max(unitHeight - FRAME_TOP_INSET, 0.001)
|
||||||
|
return { height, centerY: height / 2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
function boardGeometry(
|
||||||
|
width: number,
|
||||||
|
thickness: number,
|
||||||
|
depth: number,
|
||||||
|
insetWidth = false,
|
||||||
|
): BoxGeometry {
|
||||||
return new BoxGeometry(
|
return new BoxGeometry(
|
||||||
Math.max(width - 2 * BOARD_INSET, 0.001),
|
insetWidth ? Math.max(width - 2 * BOARD_INSET, 0.001) : width,
|
||||||
thickness,
|
thickness,
|
||||||
Math.max(depth - 2 * BOARD_INSET, 0.001),
|
Math.max(depth - 2 * BOARD_INSET, 0.001),
|
||||||
)
|
)
|
||||||
@@ -167,10 +184,14 @@ function buildBookshelf(group: Group, node: ShelfNode, materials: ShelfSlotMater
|
|||||||
const unitHeight = node.height + node.thickness
|
const unitHeight = node.height + node.thickness
|
||||||
const innerWidth = node.withSides ? node.width - 2 * node.thickness : node.width
|
const innerWidth = node.withSides ? node.width - 2 * node.thickness : node.width
|
||||||
|
|
||||||
// Top + bottom + intermediate boards
|
// Top + bottom + intermediate boards. No sides => boards span over corner
|
||||||
|
// posts, so inset their width too; with sides they abut the panels (flush).
|
||||||
for (const y of boardCenterYs(node)) {
|
for (const y of boardCenterYs(node)) {
|
||||||
const board = stampShelfSlot(
|
const board = stampShelfSlot(
|
||||||
new Mesh(boardGeometry(innerWidth, node.thickness, node.depth), materials.shelves),
|
new Mesh(
|
||||||
|
boardGeometry(innerWidth, node.thickness, node.depth, !node.withSides),
|
||||||
|
materials.shelves,
|
||||||
|
),
|
||||||
'shelves',
|
'shelves',
|
||||||
)
|
)
|
||||||
board.name = `shelf-board-${boardRowIndex(node, y)}`
|
board.name = `shelf-board-${boardRowIndex(node, y)}`
|
||||||
@@ -204,26 +225,28 @@ function buildBookshelf(group: Group, node: ShelfNode, materials: ShelfSlotMater
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.withBack) {
|
if (node.withBack) {
|
||||||
|
const fy = cappedFrameY(unitHeight)
|
||||||
const back = stampShelfSlot(
|
const back = stampShelfSlot(
|
||||||
new Mesh(new BoxGeometry(innerWidth, unitHeight, node.thickness), materials.back),
|
new Mesh(new BoxGeometry(innerWidth, fy.height, node.thickness), materials.back),
|
||||||
'back',
|
'back',
|
||||||
)
|
)
|
||||||
back.name = 'shelf-back'
|
back.name = 'shelf-back'
|
||||||
back.position.set(0, unitHeight / 2, -(node.depth / 2 - node.thickness / 2))
|
back.position.set(0, fy.centerY, -(node.depth / 2 - node.thickness / 2))
|
||||||
group.add(back)
|
group.add(back)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical dividers between columns
|
// Vertical dividers between columns
|
||||||
if (node.columns > 1) {
|
if (node.columns > 1) {
|
||||||
|
const fy = cappedFrameY(unitHeight)
|
||||||
const colStep = innerWidth / node.columns
|
const colStep = innerWidth / node.columns
|
||||||
for (let c = 1; c < node.columns; c++) {
|
for (let c = 1; c < node.columns; c++) {
|
||||||
const x = -innerWidth / 2 + c * colStep
|
const x = -innerWidth / 2 + c * colStep
|
||||||
const divider = stampShelfSlot(
|
const divider = stampShelfSlot(
|
||||||
new Mesh(new BoxGeometry(node.thickness, unitHeight, node.depth), materials.frame),
|
new Mesh(new BoxGeometry(node.thickness, fy.height, node.depth), materials.frame),
|
||||||
'frame',
|
'frame',
|
||||||
)
|
)
|
||||||
divider.name = `shelf-divider-col-${c}`
|
divider.name = `shelf-divider-col-${c}`
|
||||||
divider.position.set(x, unitHeight / 2, 0)
|
divider.position.set(x, fy.centerY, 0)
|
||||||
group.add(divider)
|
group.add(divider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,7 +264,7 @@ function buildOpenRack(group: Group, node: ShelfNode, materials: ShelfSlotMateri
|
|||||||
|
|
||||||
for (const y of boardCenterYs(node)) {
|
for (const y of boardCenterYs(node)) {
|
||||||
const board = stampShelfSlot(
|
const board = stampShelfSlot(
|
||||||
new Mesh(boardGeometry(innerWidth, boardThickness, node.depth), materials.shelves),
|
new Mesh(boardGeometry(innerWidth, boardThickness, node.depth, true), materials.shelves),
|
||||||
'shelves',
|
'shelves',
|
||||||
)
|
)
|
||||||
board.name = `shelf-board-${boardRowIndex(node, y)}`
|
board.name = `shelf-board-${boardRowIndex(node, y)}`
|
||||||
@@ -307,12 +330,13 @@ function buildCubby(group: Group, node: ShelfNode, materials: ShelfSlotMaterials
|
|||||||
group.add(side)
|
group.add(side)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fy = cappedFrameY(unitHeight)
|
||||||
const back = stampShelfSlot(
|
const back = stampShelfSlot(
|
||||||
new Mesh(new BoxGeometry(innerWidth, unitHeight, node.thickness), materials.back),
|
new Mesh(new BoxGeometry(innerWidth, fy.height, node.thickness), materials.back),
|
||||||
'back',
|
'back',
|
||||||
)
|
)
|
||||||
back.name = 'shelf-back'
|
back.name = 'shelf-back'
|
||||||
back.position.set(0, unitHeight / 2, -(node.depth / 2 - node.thickness / 2))
|
back.position.set(0, fy.centerY, -(node.depth / 2 - node.thickness / 2))
|
||||||
group.add(back)
|
group.add(back)
|
||||||
|
|
||||||
if (node.columns > 1) {
|
if (node.columns > 1) {
|
||||||
@@ -371,19 +395,20 @@ function addCornerPosts(
|
|||||||
unitHeight: number,
|
unitHeight: number,
|
||||||
postStyle: 'rack' | 'leg',
|
postStyle: 'rack' | 'leg',
|
||||||
) {
|
) {
|
||||||
|
const fy = cappedFrameY(unitHeight)
|
||||||
const postThickness =
|
const postThickness =
|
||||||
postStyle === 'rack' ? Math.max(0.025, node.thickness * 1.5) : Math.max(0.02, node.thickness)
|
postStyle === 'rack' ? Math.max(0.025, node.thickness * 1.5) : Math.max(0.02, node.thickness)
|
||||||
const inset = postThickness / 2
|
const inset = postThickness / 2
|
||||||
for (const xSign of [-1, 1] as const) {
|
for (const xSign of [-1, 1] as const) {
|
||||||
for (const zSign of [-1, 1] as const) {
|
for (const zSign of [-1, 1] as const) {
|
||||||
const post = stampShelfSlot(
|
const post = stampShelfSlot(
|
||||||
new Mesh(new BoxGeometry(postThickness, unitHeight, postThickness), material),
|
new Mesh(new BoxGeometry(postThickness, fy.height, postThickness), material),
|
||||||
'frame',
|
'frame',
|
||||||
)
|
)
|
||||||
post.name = `shelf-post-${xSign === -1 ? 'l' : 'r'}${zSign === -1 ? 'b' : 'f'}`
|
post.name = `shelf-post-${xSign === -1 ? 'l' : 'r'}${zSign === -1 ? 'b' : 'f'}`
|
||||||
post.position.set(
|
post.position.set(
|
||||||
xSign * (node.width / 2 - inset),
|
xSign * (node.width / 2 - inset),
|
||||||
unitHeight / 2,
|
fy.centerY,
|
||||||
zSign * (node.depth / 2 - inset),
|
zSign * (node.depth / 2 - inset),
|
||||||
)
|
)
|
||||||
group.add(post)
|
group.add(post)
|
||||||
|
|||||||
Reference in New Issue
Block a user