All files / web/src/components/create/abacus ConstructionControl.tsx

100% Statements 108/108
100% Branches 11/11
100% Functions 4/4
100% Lines 108/108

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 1091x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 2x 2x 2x 2x 1x 1x 1x 2x 1x 1x 2x 10x 10x 10x 10x 10x 1x 9x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x  
'use client'
 
// ConstructionControl — one-piece vs column modules (Gitea #30), in the DESIGN
// rail's "Shape" section.
//
// WHY THE DESIGN RAIL. `seam_mode` is a geometry Param: it changes the
// footprint (a web per seam), it greys four engraving slots, and it decides
// whether the object in the hand comes apart. That is what the thing IS, not
// how it is made — so it belongs beside size and columns, above the fabrication
// question, and it is asked once whatever the output. What modular COSTS a
// printer — the joint-fit tuning, the seam coupon, the kit zip — stays in the
// print rail, where the rest of "how it's made" lives.
//
// PRESENTATION. An option list (StudioChoice), not a switch. Each option says
// what it is and what it measures BEFORE it is chosen, and the controls that
// only exist because modules were chosen — the joint, its three-sentence
// description, the pointer to the print rail — live inside the Column modules
// option itself. Nothing below the list can be mistaken for part of it.
//
// The joint description is deliberately short: the studio's job here is to make
// the choice legible, not to teach the mechanism. Three sentences: how it goes
// together, how it holds, how it comes apart.
 
import { StudioChoice } from '@/components/studio/StudioChoice'
import { StudioSelect } from '@/components/studio/StudioSelect'
import { STUDIO } from '@/components/studio/theme'
import { useAbacusStudio } from './AbacusStudioContext'
import { isModular, type JointType, SLIDING_FIT_VALUES } from './abacus-model'
import { modularSizeDelta } from './ModularSeamPanel'
 
const mm = (v: number) => v.toFixed(1)
 
const NOTE = STUDIO.type.note
 
export function ConstructionControl() {
  const { params, set } = useAbacusStudio()
  const on = isModular(params)
  const sliding = params.joint_type === 'sliding_dovetail'
  // Both sizes come from the ONE derived chain the geometry uses, evaluated at
  // each mode — a second copy of the arithmetic here is exactly how a readout
  // and the thing it describes drift apart.
  const delta = modularSizeDelta(params)
  const wider = delta.modular[0] - delta.mono[0]
 
  return (
    <div data-element="abacus-construction">
      <StudioChoice
        label="construction"
        value={on ? 'modular' : 'mono'}
        onChange={(v) => set('seam_mode', v)}
        dataElement="abacus-construction-choice"
        dataAction="set-construction-mode"
        options={[
          {
            value: 'mono',
            title: 'One piece',
            description: `One solid frame, ${mm(delta.mono[0])} × ${mm(delta.mono[1])} mm.`,
          },
          {
            value: 'modular',
            title: 'Column modules',
            // the seam cost as a delta against the one-piece line just above —
            // every seam keeps a full wall on both sides, and every module
            // carries its own feet
            description: `Columns that join and come apart, each on its own feet — any subset stands. ${mm(wider)} mm wider, ${mm(delta.modular[0])} × ${mm(delta.modular[1])} mm.`,
            children: (
              <>
                <StudioSelect
                  label="joint"
                  value={params.joint_type}
                  options={[
                    { value: 'vertical_snap', label: 'Vertical dovetail with snap clip' },
                    { value: 'sliding_dovetail', label: 'Sliding dovetail, rear entry' },
                  ]}
                  onChange={(v) => {
                    const joint = v as JointType
                    set('joint_type', joint)
                    if (
                      joint === 'sliding_dovetail' &&
                      !SLIDING_FIT_VALUES.some(
                        (fitValue) => Math.abs(fitValue - params.joint_fit) < 1e-9
                      )
                    ) {
                      set('joint_fit', SLIDING_FIT_VALUES[0])
                    }
                  }}
                  dataElement="modular-joint-type"
                  dataAction="select-modular-joint-type"
                />
                <div data-element="modular-joint-explanation" style={NOTE}>
                  {sliding
                    ? 'Each module slides in from the back along a tapered dovetail rail and clicks into a notch at the front stop. The taper holds itself; a firm rearward tug releases it. No seam opens through the underside.'
                    : 'Tilt the module ~45°, hook the thin foot of its dovetail posts into the open pockets, then roll it upright — the posts wedge home as it straightens, and the crossbar clip clicks at full depth. Any middle module rolls in — or out — of an assembled row.'}
                </div>
                {/* the tuning and the coupon are a printer's problem, so they
                    live in the print rail — say where, or the choice looks
                    unfinished */}
                <div data-element="modular-print-pointer" style={NOTE}>
                  Tune the fit and print the seam coupon under Print options.
                </div>
              </>
            ),
          },
        ]}
      />
    </div>
  )
}