Skip to content

Commit b3c0e18

Browse files
committed
Merge branch 'main' into prod
2 parents 149e33d + 8f64d0b commit b3c0e18

53 files changed

Lines changed: 1372 additions & 234 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/components/Canvas/KonvaObject.tsx‎

Lines changed: 108 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect } from "react";
2+
import type Konva from "konva";
23
import { useFontCacheVersion } from "../../hooks/useFontCacheVersion";
34
import { Ellipse, Group, Rect, Shape, Text } from "react-konva";
45
import { lookupBoundVariable, shouldShowFallbackTint } from "../../lib/variableBinding";
@@ -18,7 +19,8 @@ import { selectionHandlers, type KonvaObjectProps } from "./konvaObjectProps";
1819
import { setMeasuredBounds, clearMeasuredBounds } from "./measuredBoundsCache";
1920
import { DEFAULT_GS_SYMBOL_META, GS_SYMBOLS } from "../../registry/symbol";
2021
import { GS_SYMBOL_PATHS, GS_VECTOR_CODES, type GsVectorCode } from "../../registry/gsSymbolPaths";
21-
import { blockBoundsDots, blockJustifyWordPositions, blockLineStartDots, blockLineStepDots, wrapBlockLines, zebraAlignOffsetDots, zebraHangingIndentOffsetDots, zebraJustifyGapDots, zebraLineWidthDots, type ZplRotation } from "../../lib/zebraTextLayout";
22+
import { blockBoundsDots, blockJustifyWordPositions, blockLineStartDots, blockLineStepDots, tbBoundsDots, tbLineStepDots, wrapBlockLines, zebraAlignOffsetDots, zebraHangingIndentOffsetDots, zebraJustifyGapDots, zebraLineWidthDots, type ZplRotation } from "../../lib/zebraTextLayout";
23+
import { resolveTextMode } from "../../registry/text";
2224
import type { LeafObject } from "../../registry";
2325
import type { TextProps } from "../../registry/text";
2426
import type { SerialProps } from "../../registry/serial";
@@ -168,8 +170,9 @@ function TextFieldContent({
168170
if (obj.type !== "text") {
169171
return <Text key={fontVersion} x={shift} y={shiftY} text={content} {...base} />;
170172
}
171-
const { blockWidth, blockLines, blockJustify, blockLineSpacing, blockHangingIndent, fontHeight, fontWidth } = obj.props;
172-
if (!blockWidth) {
173+
const { blockWidth, blockLines, blockHeight, blockJustify, blockLineSpacing, blockHangingIndent, fontHeight, fontWidth } = obj.props;
174+
const mode = resolveTextMode(obj.props);
175+
if (mode === "normal" || !blockWidth) {
173176
// Single-line text has no spec-defined min width, so only empty
174177
// content triggers the placeholder (block also covers too-narrow).
175178
if (content.trim().length === 0) {
@@ -205,13 +208,16 @@ function TextFieldContent({
205208
dotsToPx(blockWidth, scale, dpmm) < measureLinePx(firstChar);
206209
const emptyContent = content.trim().length === 0;
207210
if (tooNarrow || emptyContent) {
208-
const bounds = blockBoundsDots({
209-
blockWidthDots: blockWidth,
210-
blockLines: blockLines ?? 1,
211-
blockLineSpacing: blockLineSpacing ?? 0,
212-
fontHeight,
213-
rotation: obj.props.rotation,
214-
});
211+
const bounds =
212+
mode === "tb"
213+
? tbBoundsDots(blockWidth, blockHeight ?? fontHeight, obj.props.rotation)
214+
: blockBoundsDots({
215+
blockWidthDots: blockWidth,
216+
blockLines: blockLines ?? 1,
217+
blockLineSpacing: blockLineSpacing ?? 0,
218+
fontHeight,
219+
rotation: obj.props.rotation,
220+
});
215221
return (
216222
<PlaceholderRect
217223
fontVersion={fontVersion}
@@ -223,23 +229,90 @@ function TextFieldContent({
223229
/>
224230
);
225231
}
232+
// Konva's getClientRect ignores a Group's clip and returns the full children
233+
// bounds, which would inflate the Transformer box past the ^TB clip. Report
234+
// the clip rect instead (the actual visible region) so the box matches.
235+
const tbClipRef = (node: Konva.Group | null) => {
236+
if (!node) return;
237+
node.getClientRect = (config?: { skipTransform?: boolean; relativeTo?: Konva.Container }) => {
238+
const local = {
239+
x: node.clipX() || 0,
240+
y: node.clipY() || 0,
241+
width: node.clipWidth(),
242+
height: node.clipHeight(),
243+
};
244+
if (config?.skipTransform) return local;
245+
const tr = config?.relativeTo
246+
? node.getAbsoluteTransform(config.relativeTo)
247+
: node.getAbsoluteTransform();
248+
const corners = [
249+
{ x: local.x, y: local.y },
250+
{ x: local.x + local.width, y: local.y },
251+
{ x: local.x + local.width, y: local.y + local.height },
252+
{ x: local.x, y: local.y + local.height },
253+
].map((p) => tr.point(p));
254+
const xs = corners.map((c) => c.x);
255+
const ys = corners.map((c) => c.y);
256+
const minX = Math.min(...xs);
257+
const minY = Math.min(...ys);
258+
return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
259+
};
260+
};
261+
262+
// ^TB: word-wrap left-aligned, no spacing/indent/justify, clipped at the
263+
// block height (Labelary truncates mid-glyph, so clip rather than drop whole
264+
// lines). At 90deg rotations the block rect stays axis-aligned.
265+
if (mode === "tb") {
266+
// ^TB has no hard break; emit collapses newlines to spaces, so the canvas
267+
// must too or it would show line breaks that won't print.
268+
const tbText = content.replace(/\n/g, " ");
269+
const tbLines = wrapBlockLines(tbText, dotsToPx(blockWidth, scale, dpmm), measureLinePx);
270+
const tbStep = tbLineStepDots(fontHeight);
271+
const clip = tbBoundsDots(blockWidth, blockHeight ?? fontHeight, obj.props.rotation);
272+
// Lines sit exactly like ^FB (first line at the block top with Konva's cap
273+
// pad); matches the Labelary preview so toggling it doesn't shift the text.
274+
return (
275+
<Group
276+
ref={tbClipRef}
277+
clipX={dotsToPx(clip.x, scale, dpmm) + shift}
278+
clipY={dotsToPx(clip.y, scale, dpmm) + shiftY}
279+
clipWidth={dotsToPx(clip.width, scale, dpmm)}
280+
clipHeight={dotsToPx(clip.height, scale, dpmm)}
281+
>
282+
{tbLines.map((line, i) => {
283+
const startDots = blockLineStartDots(i, obj.props.rotation, 0, tbStep);
284+
return (
285+
<Text
286+
key={`${fontVersion}-${i}`}
287+
x={dotsToPx(startDots.x, scale, dpmm) + shift}
288+
y={dotsToPx(startDots.y, scale, dpmm) + shiftY}
289+
text={line}
290+
{...base}
291+
/>
292+
);
293+
})}
294+
</Group>
295+
);
296+
}
226297
const justify = blockJustify ?? "L";
227298
const indent = blockHangingIndent ?? 0;
228299
const lineStepDots = blockLineStepDots(fontHeight, blockLineSpacing ?? 0);
229-
// Wrap to the rendered block width; ^FB slot b: overflow lines overprint
230-
// the last line, so keep only blockLines worth.
300+
// Wrap to the rendered block width; ^FB slot b: text exceeding blockLines
301+
// overprints onto the last line (matches Labelary), so pin overflow rows to
302+
// the last index rather than dropping them.
303+
const cap = blockLines ?? 1;
231304
const lines = wrapBlockLines(
232305
content,
233306
dotsToPx(blockWidth, scale, dpmm),
234307
measureLinePx,
235-
).slice(0, blockLines ?? 1);
308+
).map((line, i) => ({ line, row: Math.min(i, cap - 1) }));
236309
// Labelary centers / justifies as if a trailing space is present; feed the
237310
// rendered space width so both match the preview on scaled / device fonts.
238311
const spaceWidthDots = pxToDots(measureLinePx(" "), scale, dpmm);
239312
return (
240313
<>
241-
{lines.flatMap((line, i) => {
242-
const indentDots = zebraHangingIndentOffsetDots(i, indent);
314+
{lines.flatMap(({ line, row }, i) => {
315+
const indentDots = zebraHangingIndentOffsetDots(row, indent);
243316
// Measure the rendered glyphs (same basis as the wrap) so justify
244317
// offsets match what is drawn; zebraLineWidthDots over-estimates
245318
// scaled / device fonts and would clamp the offset to 0 (stuck left).
@@ -252,7 +325,7 @@ function TextFieldContent({
252325
const words = line.split(" ");
253326
const extraGap = zebraJustifyGapDots(lineWidthDots, effectiveBlockWidth, words.length - 1, isLast);
254327
if (extraGap > 0) {
255-
const startDots = blockLineStartDots(i, obj.props.rotation, indentDots, lineStepDots);
328+
const startDots = blockLineStartDots(row, obj.props.rotation, indentDots, lineStepDots);
256329
const positions = blockJustifyWordPositions({
257330
words, rotation: obj.props.rotation, startDots,
258331
fontHeight, fontWidth, extraGapDots: extraGap,
@@ -271,7 +344,7 @@ function TextFieldContent({
271344
}
272345
}
273346
const alignOffsetDots = zebraAlignOffsetDots(lineWidthDots, effectiveBlockWidth, justify, spaceWidthDots);
274-
const startDots = blockLineStartDots(i, obj.props.rotation, indentDots + alignOffsetDots, lineStepDots);
347+
const startDots = blockLineStartDots(row, obj.props.rotation, indentDots + alignOffsetDots, lineStepDots);
275348
return [
276349
<Text
277350
key={`${fontVersion}-${i}`}
@@ -295,6 +368,7 @@ function BlockWrapGuide({
295368
blockWidthDots,
296369
blockLines,
297370
blockLineSpacing,
371+
blockHeightDots,
298372
fontHeight,
299373
rotation,
300374
scale,
@@ -305,14 +379,19 @@ function BlockWrapGuide({
305379
blockWidthDots: number;
306380
blockLines: number;
307381
blockLineSpacing: number;
382+
/** Set for ^TB: the frame is the width x clip-height rect, not line-stacked. */
383+
blockHeightDots?: number;
308384
fontHeight: number;
309385
rotation: ZplRotation;
310386
scale: number;
311387
dpmm: number;
312388
color: string;
313389
frameMeasured: boolean;
314390
}) {
315-
const bounds = blockBoundsDots({ blockWidthDots, blockLines, blockLineSpacing, fontHeight, rotation });
391+
const bounds =
392+
blockHeightDots != null
393+
? tbBoundsDots(blockWidthDots, blockHeightDots, rotation)
394+
: blockBoundsDots({ blockWidthDots, blockLines, blockLineSpacing, fontHeight, rotation });
316395
const bx = dotsToPx(bounds.x, scale, dpmm);
317396
const by = dotsToPx(bounds.y, scale, dpmm);
318397
const bw = dotsToPx(bounds.width, scale, dpmm);
@@ -553,13 +632,16 @@ function KonvaObjectInner({
553632
// non-reverse path, knocked out in white. Reuse blockBoundsDots (the same
554633
// box the generator emits) and TextFieldContent so the preview matches
555634
// both the print and the normal render.
556-
const bounds = blockBoundsDots({
557-
blockWidthDots: obj.props.blockWidth,
558-
blockLines: obj.props.blockLines ?? 1,
559-
blockLineSpacing: obj.props.blockLineSpacing ?? 0,
560-
fontHeight: obj.props.fontHeight,
561-
rotation: p.rotation,
562-
});
635+
const bounds =
636+
resolveTextMode(obj.props) === "tb"
637+
? tbBoundsDots(obj.props.blockWidth, obj.props.blockHeight ?? obj.props.fontHeight, p.rotation)
638+
: blockBoundsDots({
639+
blockWidthDots: obj.props.blockWidth,
640+
blockLines: obj.props.blockLines ?? 1,
641+
blockLineSpacing: obj.props.blockLineSpacing ?? 0,
642+
fontHeight: obj.props.fontHeight,
643+
rotation: p.rotation,
644+
});
563645
return (
564646
<Group
565647
id={obj.id}
@@ -691,6 +773,7 @@ function KonvaObjectInner({
691773
blockWidthDots={obj.props.blockWidth}
692774
blockLines={obj.props.blockLines ?? 1}
693775
blockLineSpacing={obj.props.blockLineSpacing ?? 0}
776+
blockHeightDots={resolveTextMode(obj.props) === "tb" ? obj.props.blockHeight ?? obj.props.fontHeight : undefined}
694777
fontHeight={obj.props.fontHeight}
695778
rotation={obj.props.rotation}
696779
scale={scale}

‎src/components/Canvas/LabelCanvas.tsx‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
115115
// Action-bar chrome refs live here so the drag onDelta can translate the bar
116116
// live (its own beforeDraw lags for controller-moved siblings).
117117
const actionBarRef = useRef<Konva.Group>(null);
118+
// The view-rotation group; the action bar lives outside it (stage coords), so
119+
// its rest bounds must be mapped through this group's transform when rotated.
120+
const rotationGroupRef = useRef<Konva.Group>(null);
118121
const lockedFrameRef = useRef<Konva.Group>(null);
119122
const dragActiveRef = useRef(false);
120123
const transformActiveRef = useRef(false);
@@ -451,6 +454,28 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
451454
}
452455
// Action-bar bounds: live client-rects during a transformer resize (model
453456
// commits only at the end), else optical model bounds (matches the drag center).
457+
// Map group-local bounds into stage coords (identity when unrotated). Single
458+
// source for the rest position (getBarBounds) and on-drag position (onDelta).
459+
const barBoundsToStage = (b: BarBounds): BarBounds => {
460+
const rg = rotationGroupRef.current;
461+
if (!rg || viewRotation === 0) return b;
462+
const tf = rg.getAbsoluteTransform();
463+
const pts = [
464+
tf.point({ x: b.minX, y: b.minY }),
465+
tf.point({ x: b.maxX, y: b.minY }),
466+
tf.point({ x: b.minX, y: b.maxY }),
467+
tf.point({ x: b.maxX, y: b.maxY }),
468+
];
469+
const xs = pts.map((p) => p.x);
470+
const ys = pts.map((p) => p.y);
471+
return {
472+
minX: Math.min(...xs),
473+
minY: Math.min(...ys),
474+
maxX: Math.max(...xs),
475+
maxY: Math.max(...ys),
476+
};
477+
};
478+
454479
const getBarBounds = (): BarBounds | null => {
455480
const stage = stageRef.current;
456481
if (transformActiveRef.current && stage) {
@@ -467,7 +492,9 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
467492
return minX === Infinity ? null : { minX, minY, maxX, maxY };
468493
}
469494
const u = toFramePx(selectionUnionDots(getCurrentObjects(), visibleSelIds, frameCtx));
470-
return u ? { minX: u.x, minY: u.y, maxX: u.x + u.width, maxY: u.y + u.height } : null;
495+
return u
496+
? barBoundsToStage({ minX: u.x, minY: u.y, maxX: u.x + u.width, maxY: u.y + u.height })
497+
: null;
471498
};
472499
useLayoutEffect(() => {
473500
const r = selectionFrameRef.current;
@@ -520,7 +547,14 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
520547
}
521548
bar.position(
522549
actionBarPosition(
523-
{ minX: live.x, minY: live.y, maxX: live.x + live.width, maxY: live.y + live.height },
550+
// `live` is group-local (the drag delta is in model space); map it to
551+
// stage coords so the bar tracks the rotated selection on-drag too.
552+
barBoundsToStage({
553+
minX: live.x,
554+
minY: live.y,
555+
maxX: live.x + live.width,
556+
maxY: live.y + live.height,
557+
}),
524558
barHalfRef.current.w,
525559
barHalfRef.current.h,
526560
stage.width(),
@@ -1037,6 +1071,7 @@ export const LabelCanvas = forwardRef<LabelCanvasHandle, Props>(function LabelCa
10371071
<Layer>
10381072
{/* Pivot at labelCenter; node.x()/y() stays group-local during drags. */}
10391073
<Group
1074+
ref={rotationGroupRef}
10401075
x={labelCenterX}
10411076
y={labelCenterY}
10421077
rotation={viewRotation}

0 commit comments

Comments
 (0)