@1agh/maude 0.58.3 → 0.60.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/apps/studio/annotations-layer.tsx +49 -15
- package/apps/studio/bin/_import-asset.mjs +18 -0
- package/apps/studio/bin/_import-figma.mjs +1180 -242
- package/apps/studio/bin/_perf-probe-safari.mjs +332 -0
- package/apps/studio/bin/_perf-probe.mjs +228 -0
- package/apps/studio/bin/_perf-shared.mjs +345 -0
- package/apps/studio/bin/_video-playwright.mjs +17 -4
- package/apps/studio/bin/import-figma.sh +10 -1
- package/apps/studio/bin/perf.sh +228 -0
- package/apps/studio/bin/smoke.sh +49 -5
- package/apps/studio/canvas-lib.tsx +148 -6
- package/apps/studio/client/app.jsx +152 -37
- package/apps/studio/client/panels/SyncPanel.jsx +320 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +29 -1
- package/apps/studio/client/panels/timeline-comp-target.js +101 -0
- package/apps/studio/client/styles/3-shell-maude.css +40 -0
- package/apps/studio/client/styles/4-components.css +4 -4
- package/apps/studio/context.ts +4 -0
- package/apps/studio/dist/client.bundle.js +772 -772
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/video-encode-lib.ts +8 -5
- package/apps/studio/exporters/video.ts +10 -0
- package/apps/studio/figma/assets.test.ts +92 -0
- package/apps/studio/figma/assets.ts +63 -9
- package/apps/studio/figma/codegen-client.test.ts +276 -0
- package/apps/studio/figma/codegen-client.ts +509 -0
- package/apps/studio/figma/codegen-fonts.test.ts +103 -0
- package/apps/studio/figma/codegen-fonts.ts +195 -0
- package/apps/studio/figma/codegen-values.test.ts +179 -0
- package/apps/studio/figma/codegen-values.ts +270 -0
- package/apps/studio/figma/endpoints.ts +73 -0
- package/apps/studio/figma/fig-decode.test.ts +788 -0
- package/apps/studio/figma/fig-decode.ts +839 -0
- package/apps/studio/figma/fig-differential.test.ts +182 -0
- package/apps/studio/figma/fig-kiwi.ts +410 -0
- package/apps/studio/figma/fig-translator.test.ts +192 -0
- package/apps/studio/figma/fig-vector.test.ts +113 -0
- package/apps/studio/figma/fig-vector.ts +145 -0
- package/apps/studio/figma/fig-zip.ts +270 -0
- package/apps/studio/figma/from-codegen.test.ts +408 -0
- package/apps/studio/figma/from-codegen.ts +1103 -0
- package/apps/studio/figma/sanitize.test.ts +69 -0
- package/apps/studio/figma/sanitize.ts +146 -47
- package/apps/studio/figma/tailwind-map.test.ts +142 -0
- package/apps/studio/figma/tailwind-map.ts +545 -0
- package/apps/studio/figma/to-artboard.ts +41 -1
- package/apps/studio/figma/to-render.ts +25 -3
- package/apps/studio/figma/types.ts +6 -1
- package/apps/studio/http.ts +94 -0
- package/apps/studio/sync/asset-push-worker.ts +84 -0
- package/apps/studio/sync/asset-push.ts +441 -39
- package/apps/studio/sync/asset-sweep.ts +262 -0
- package/apps/studio/sync/connection-state.ts +71 -3
- package/apps/studio/sync/index.ts +39 -6
- package/apps/studio/sync/presentation.ts +21 -0
- package/apps/studio/sync/status.ts +18 -0
- package/apps/studio/sync/supervisor.ts +20 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
- package/apps/studio/test/figma-explode.test.ts +438 -0
- package/apps/studio/test/fixtures/perf-canvas.mjs +201 -0
- package/apps/studio/test/import-figma.test.ts +192 -4
- package/apps/studio/test/sync-asset-push-worker.test.ts +183 -0
- package/apps/studio/test/sync-asset-push.test.ts +639 -47
- package/apps/studio/test/sync-asset-sweep.test.ts +243 -0
- package/apps/studio/test/sync-connection-state.test.ts +66 -0
- package/apps/studio/test/sync-panel-surface.test.ts +123 -0
- package/apps/studio/test/sync-resync-routes.test.ts +125 -0
- package/apps/studio/test/sync-status.test.ts +28 -0
- package/apps/studio/test/sync-supervisor.test.ts +46 -0
- package/apps/studio/test/timeline-comp-target.test.ts +139 -0
- package/apps/studio/test/video-comp.test.ts +81 -1
- package/apps/studio/test/video-encode-lib.test.ts +63 -0
- package/apps/studio/use-artboard-drag.tsx +37 -3
- package/apps/studio/video-comp.tsx +51 -0
- package/apps/studio/whats-new.json +87 -0
- package/cli/commands/design.mjs +7 -0
- package/cli/commands/kg.mjs +8 -1
- package/cli/commands/kg.test.mjs +24 -0
- package/cli/lib/figma-codegen-reachability.test.mjs +104 -0
- package/cli/lib/figma-import-controls.test.mjs +70 -0
- package/package.json +8 -8
package/apps/studio/bin/smoke.sh
CHANGED
|
@@ -26,10 +26,14 @@
|
|
|
26
26
|
# [--changed-only] (default off — screenshot only canvases changed
|
|
27
27
|
# since the last smoke run; escalates to the full
|
|
28
28
|
# set when dev-server / canvas-lib / templates changed)
|
|
29
|
+
# [--perf] (also benchmark the heaviest canvas after the sweep
|
|
30
|
+
# and append the frame-time / render-count delta to
|
|
31
|
+
# the report — see perf.sh)
|
|
29
32
|
#
|
|
30
33
|
# Reads:
|
|
31
34
|
# $DESIGN_ROOT/_server.json (must exist — caller runs server-up.sh first)
|
|
32
|
-
# $DESIGN_ROOT/ui
|
|
35
|
+
# $DESIGN_ROOT/ui/**/*.tsx (canvases, RECURSIVELY — `--pages` imports
|
|
36
|
+
# land in ui/<folder>/ and were being missed)
|
|
33
37
|
# $DESIGN_ROOT/system/*/preview/*.tsx (specimens, when --include-system 1)
|
|
34
38
|
#
|
|
35
39
|
# Writes:
|
|
@@ -52,6 +56,7 @@ INCLUDE_SYSTEM=1
|
|
|
52
56
|
TIMEOUT=8
|
|
53
57
|
ENGINE="auto"
|
|
54
58
|
CHANGED_ONLY=0
|
|
59
|
+
PERF=0
|
|
55
60
|
|
|
56
61
|
while [ $# -gt 0 ]; do
|
|
57
62
|
case "$1" in
|
|
@@ -61,6 +66,7 @@ while [ $# -gt 0 ]; do
|
|
|
61
66
|
--timeout) TIMEOUT="$2"; shift 2 ;;
|
|
62
67
|
--engine) ENGINE="$2"; shift 2 ;;
|
|
63
68
|
--changed-only) CHANGED_ONLY=1; shift ;;
|
|
69
|
+
--perf) PERF=1; shift ;;
|
|
64
70
|
--help|-h)
|
|
65
71
|
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
|
66
72
|
exit 0
|
|
@@ -111,13 +117,24 @@ fi
|
|
|
111
117
|
echo "→ smoke engine: $ENGINE | port: $PORT | out: $OUT_DIR" >&2
|
|
112
118
|
|
|
113
119
|
# ---------- collect canvases ----------
|
|
120
|
+
#
|
|
121
|
+
# RECURSIVE, and that is a fix, not a preference. This was `-maxdepth 1` and the
|
|
122
|
+
# consequence was measured on the first live Figma migration: `--pages` always
|
|
123
|
+
# writes to `ui/<folder>/`, so EVERY canvas the primary import route produces was
|
|
124
|
+
# invisible here. The run reported "52/52 canvases rendered styled" having never
|
|
125
|
+
# looked at the 6 imported pages — green, true, and worthless. The import-graph
|
|
126
|
+
# lint below already walks `ui/` recursively, which is exactly why the mismatch
|
|
127
|
+
# went unnoticed: the lint covered the imports, the screenshots did not.
|
|
128
|
+
#
|
|
129
|
+
# A `_`-prefixed DIRECTORY is skipped as well as a `_`-prefixed file, so runtime
|
|
130
|
+
# state (`_history/`, `_draw/`, `_smoke/` output itself) never enters the sweep —
|
|
131
|
+
# without that, recursing would screenshot our own previous reports.
|
|
114
132
|
CANVASES=""
|
|
115
133
|
if [ -d "$DESIGN_ROOT/ui" ]; then
|
|
116
134
|
while IFS= read -r f; do
|
|
117
|
-
|
|
118
|
-
case "$base" in _*) continue ;; esac
|
|
135
|
+
case "$f" in *'/_'*) continue ;; esac
|
|
119
136
|
CANVASES="$CANVASES$f"$'\n'
|
|
120
|
-
done < <(find "$DESIGN_ROOT/ui" -
|
|
137
|
+
done < <(find "$DESIGN_ROOT/ui" -type f -name '*.tsx' 2>/dev/null | sort)
|
|
121
138
|
fi
|
|
122
139
|
|
|
123
140
|
if [ "$INCLUDE_SYSTEM" = "1" ] && [ -d "$DESIGN_ROOT/system" ]; then
|
|
@@ -130,7 +147,7 @@ fi
|
|
|
130
147
|
|
|
131
148
|
CANVASES=$(printf '%s' "$CANVASES" | sed '/^$/d')
|
|
132
149
|
COUNT=$(printf '%s' "$CANVASES" | grep -c .)
|
|
133
|
-
[ "$COUNT" -gt 0 ] || { echo "smoke.sh: no canvases found (ui
|
|
150
|
+
[ "$COUNT" -gt 0 ] || { echo "smoke.sh: no canvases found (ui/**/*.tsx or system/*/preview/*.tsx)" >&2; exit 1; }
|
|
134
151
|
echo "→ found $COUNT canvases" >&2
|
|
135
152
|
|
|
136
153
|
# ---------- --changed-only incremental filter (Phase C / DDR-061) ----------
|
|
@@ -525,6 +542,33 @@ TOTAL_FAIL=$((FAILED + LINT_FAILED))
|
|
|
525
542
|
fi
|
|
526
543
|
} >> "$MD"
|
|
527
544
|
|
|
545
|
+
# ---------- optional perf pass (--perf) ----------
|
|
546
|
+
# Smoke answers "does it still render"; this answers "is it still fast", against
|
|
547
|
+
# the same canvases, in the same run. The heaviest canvas (most artboards) is the
|
|
548
|
+
# one worth timing — a 1-artboard specimen has nothing to say about pan/zoom.
|
|
549
|
+
if [ "$PERF" = "1" ]; then
|
|
550
|
+
HEAVIEST=""
|
|
551
|
+
HEAVIEST_N=0
|
|
552
|
+
while IFS= read -r f; do
|
|
553
|
+
[ -n "$f" ] || continue
|
|
554
|
+
n=$(grep -c 'DCArtboard' "$f" 2>/dev/null || echo 0)
|
|
555
|
+
if [ "$n" -gt "$HEAVIEST_N" ]; then HEAVIEST_N="$n"; HEAVIEST="$f"; fi
|
|
556
|
+
done <<< "$CANVASES"
|
|
557
|
+
if [ -n "$HEAVIEST" ]; then
|
|
558
|
+
REL_PERF="${HEAVIEST#"$REPO"/}"
|
|
559
|
+
echo "→ perf: benchmarking heaviest canvas ($HEAVIEST_N artboards): $REL_PERF" >&2
|
|
560
|
+
{
|
|
561
|
+
echo ""
|
|
562
|
+
echo "## Perf"
|
|
563
|
+
echo ""
|
|
564
|
+
echo '```'
|
|
565
|
+
bash "$(dirname "${BASH_SOURCE[0]}")/perf.sh" --root "$REPO" --canvas "$REL_PERF" 2>&1 || \
|
|
566
|
+
echo "(perf run failed — see stderr; smoke result above is unaffected)"
|
|
567
|
+
echo '```'
|
|
568
|
+
} >> "$MD"
|
|
569
|
+
fi
|
|
570
|
+
fi
|
|
571
|
+
|
|
528
572
|
echo "→ report: $MD" >&2
|
|
529
573
|
echo "→ tsv: $TSV" >&2
|
|
530
574
|
|
|
@@ -233,6 +233,37 @@ const IS_WEBKIT =
|
|
|
233
233
|
(/apple/i.test(navigator.vendor || '') ||
|
|
234
234
|
(typeof window !== 'undefined' && 'GestureEvent' in window));
|
|
235
235
|
|
|
236
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
+
// Render instrumentation (feature-canvas-render-performance).
|
|
238
|
+
//
|
|
239
|
+
// The whole point of this work is the invariant "a pan/zoom gesture triggers no
|
|
240
|
+
// React render", and an invariant nothing measures is just an intention. The
|
|
241
|
+
// counter is OFF unless a probe installs `window.__dcPerf` first, so the
|
|
242
|
+
// production cost is one property read per render — far below the reconciliation
|
|
243
|
+
// it exists to count. `maude design perf` installs the object, drives a scripted
|
|
244
|
+
// gesture, and reports the count; an uninstrumented build reports null rather
|
|
245
|
+
// than 0, because "not measured" must never read as "measured zero".
|
|
246
|
+
type DcPerfCounters = {
|
|
247
|
+
artboardRenders: number;
|
|
248
|
+
annotationRenders: number;
|
|
249
|
+
instrumented?: boolean;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export function countRender(key: keyof DcPerfCounters): void {
|
|
253
|
+
if (typeof window === 'undefined') return;
|
|
254
|
+
const perf = (window as unknown as { __dcPerf?: DcPerfCounters }).__dcPerf;
|
|
255
|
+
if (!perf) return;
|
|
256
|
+
// Never let instrumentation break rendering. The page owns this object, so a
|
|
257
|
+
// canvas could hand back a frozen one — assigning to it throws in strict-mode
|
|
258
|
+
// ESM, and that would take out every DCArtboard render with it.
|
|
259
|
+
try {
|
|
260
|
+
perf.instrumented = true;
|
|
261
|
+
perf[key] = ((perf[key] as number) || 0) + 1;
|
|
262
|
+
} catch {
|
|
263
|
+
/* frozen or exotic object — measuring is optional, rendering is not */
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
236
267
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
268
|
// Engine CSS (Phase 4) — injected once per iframe inside DesignCanvas's mount.
|
|
238
269
|
// The visual chrome of `.dc-artboard` (borders, label strip, SKU type) still
|
|
@@ -917,6 +948,84 @@ export function getLiveViewport(): ViewportState | null {
|
|
|
917
948
|
return liveViewport ? { ...liveViewport } : null;
|
|
918
949
|
}
|
|
919
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Viewport for the few consumers that must track a gesture AS IT HAPPENS —
|
|
953
|
+
* the minimap indicator and the zoom % readout. Everything else reads the
|
|
954
|
+
* published (settle-cadence) viewport and stays still while the user moves.
|
|
955
|
+
*
|
|
956
|
+
* Polls the live mirror on rAF, but only while a gesture is in flight, and
|
|
957
|
+
* only re-renders when the value actually changed and at most every
|
|
958
|
+
* `minIntervalMs`. That keeps these two small components on the same ~20 Hz
|
|
959
|
+
* cadence they had when EVERY component was re-rendering at 20 Hz — the
|
|
960
|
+
* difference is that now it is two of them instead of all 128 artboards.
|
|
961
|
+
*/
|
|
962
|
+
// ONE rAF ticker for every live-viewport subscriber. Each mounted consumer
|
|
963
|
+
// running its own loop would multiply with the UI: a board with N sections
|
|
964
|
+
// renders N label chips, and N independent rAF callbacks per frame is a cost
|
|
965
|
+
// that grows with content while measuring the same single value.
|
|
966
|
+
const liveVpSubs = new Set<(v: ViewportState) => void>();
|
|
967
|
+
let liveVpRaf = 0;
|
|
968
|
+
let liveVpLastAt = 0;
|
|
969
|
+
let liveVpLastKey = '';
|
|
970
|
+
|
|
971
|
+
function liveVpTick(t: number): void {
|
|
972
|
+
liveVpRaf = requestAnimationFrame(liveVpTick);
|
|
973
|
+
if (t - liveVpLastAt < PUBLISH_MS) return;
|
|
974
|
+
liveVpLastAt = t;
|
|
975
|
+
const cur = liveViewport;
|
|
976
|
+
if (!cur) return;
|
|
977
|
+
const key = `${cur.x}|${cur.y}|${cur.zoom}`;
|
|
978
|
+
if (key === liveVpLastKey) return;
|
|
979
|
+
liveVpLastKey = key;
|
|
980
|
+
for (const fn of liveVpSubs) fn({ ...cur });
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function subscribeLiveViewport(fn: (v: ViewportState) => void): () => void {
|
|
984
|
+
liveVpSubs.add(fn);
|
|
985
|
+
if (liveVpRaf === 0 && typeof requestAnimationFrame === 'function') {
|
|
986
|
+
liveVpLastKey = '';
|
|
987
|
+
liveVpRaf = requestAnimationFrame(liveVpTick);
|
|
988
|
+
}
|
|
989
|
+
return () => {
|
|
990
|
+
liveVpSubs.delete(fn);
|
|
991
|
+
if (liveVpSubs.size === 0 && liveVpRaf !== 0) {
|
|
992
|
+
cancelAnimationFrame(liveVpRaf);
|
|
993
|
+
liveVpRaf = 0;
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Viewport for the few consumers that must track a gesture AS IT HAPPENS —
|
|
1000
|
+
* the minimap indicator, the zoom % readout, and the counter-scaled annotation
|
|
1001
|
+
* chrome that has to hold a constant screen size while the world scales.
|
|
1002
|
+
* Everything else reads the published (settle-cadence) viewport and stays still
|
|
1003
|
+
* while the user moves.
|
|
1004
|
+
*
|
|
1005
|
+
* Subscribes to the shared ticker only while a gesture is in flight, and only
|
|
1006
|
+
* re-renders when the value actually changed — the same ~20 Hz cadence these
|
|
1007
|
+
* components had back when EVERY component re-rendered at 20 Hz. The difference
|
|
1008
|
+
* is that it is now a handful of them instead of all 128 artboards.
|
|
1009
|
+
*/
|
|
1010
|
+
export function useLiveViewport(): ViewportState {
|
|
1011
|
+
const controller = useViewportControllerContext();
|
|
1012
|
+
const published = controller?.viewport;
|
|
1013
|
+
const interacting = !!controller?.isInteracting;
|
|
1014
|
+
const [live, setLive] = useState<ViewportState | null>(null);
|
|
1015
|
+
|
|
1016
|
+
useEffect(() => {
|
|
1017
|
+
if (!interacting) {
|
|
1018
|
+
// Settled: the published value is authoritative again. Dropping the local
|
|
1019
|
+
// copy avoids rendering a stale mid-gesture frame forever.
|
|
1020
|
+
setLive(null);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
return subscribeLiveViewport(setLive);
|
|
1024
|
+
}, [interacting]);
|
|
1025
|
+
|
|
1026
|
+
return live ?? published ?? { x: 0, y: 0, zoom: 1 };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
920
1029
|
export function useViewportController(opts: ViewportControllerOptions): ViewportControllerHandle {
|
|
921
1030
|
const {
|
|
922
1031
|
hostRef,
|
|
@@ -999,6 +1108,14 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
999
1108
|
el.style.transform = `translate(${v.x / z}px, ${v.y / z}px)`;
|
|
1000
1109
|
el.style.zoom = String(z);
|
|
1001
1110
|
}
|
|
1111
|
+
// Live zoom for counter-scaled chrome (section labels, connector dots,
|
|
1112
|
+
// selection bboxes — anything that must stay a constant number of SCREEN px
|
|
1113
|
+
// while the world scales). It has to be written per frame, right here: the
|
|
1114
|
+
// published viewport is settle-only now, so chrome sized from React state
|
|
1115
|
+
// would scale with the world through a pinch and snap back at the end —
|
|
1116
|
+
// reintroducing exactly the fidelity-pop that was removed once already
|
|
1117
|
+
// (RCA addendum: the LOD toggle was reverted for visible flicker).
|
|
1118
|
+
el.style.setProperty('--dc-zoom', String(z));
|
|
1002
1119
|
el.style.visibility = 'visible';
|
|
1003
1120
|
}, []);
|
|
1004
1121
|
|
|
@@ -1047,11 +1164,16 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
1047
1164
|
if (vpRef.current.zoom !== zoomAtInteractStartRef.current) {
|
|
1048
1165
|
writeTransform(vpRef.current, { crisp: true });
|
|
1049
1166
|
}
|
|
1167
|
+
// The gesture is over — NOW publish. Everything that reads the published
|
|
1168
|
+
// viewport (LOD banding, collab awareness, the active-artboard pick) is
|
|
1169
|
+
// settle-cadence by design; deferring the publish to here is what keeps
|
|
1170
|
+
// the whole React tree still while the user is actually moving.
|
|
1171
|
+
setViewportPublished({ ...vpRef.current });
|
|
1050
1172
|
}, 220);
|
|
1051
1173
|
}, [writeTransform]);
|
|
1052
1174
|
|
|
1053
1175
|
const applyViewport = useCallback(
|
|
1054
|
-
(next: ViewportState) => {
|
|
1176
|
+
(next: ViewportState, opts?: { defer?: boolean }) => {
|
|
1055
1177
|
const clamped: ViewportState = {
|
|
1056
1178
|
x: Number.isFinite(next.x) ? next.x : 0,
|
|
1057
1179
|
y: Number.isFinite(next.y) ? next.y : 0,
|
|
@@ -1060,7 +1182,17 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
1060
1182
|
vpRef.current = clamped;
|
|
1061
1183
|
liveViewport = clamped; // RC3 — survives the soft-reload remount
|
|
1062
1184
|
writeTransform(clamped);
|
|
1063
|
-
|
|
1185
|
+
// `defer` is what makes a gesture render-free. A wheel/pinch/drag burst
|
|
1186
|
+
// writes the transform imperatively every frame (above) and publishes to
|
|
1187
|
+
// React only once the motion settles; a programmatic move (fit, reset,
|
|
1188
|
+
// jumpTo, animateTo) publishes immediately, because nothing follows it to
|
|
1189
|
+
// trigger the settle and consumers would sit on a stale value.
|
|
1190
|
+
//
|
|
1191
|
+
// Measured reason: at 20 Hz publishing, one pan+zoom over a 128-artboard
|
|
1192
|
+
// canvas re-rendered DCArtboard 6272 times (128 boards x ~49 publishes),
|
|
1193
|
+
// and every published value was one the drag/hit-test paths already read
|
|
1194
|
+
// live from a ref. See `maude design perf` and the plan's findings table.
|
|
1195
|
+
if (!opts?.defer) schedulePublish();
|
|
1064
1196
|
scheduleSettle();
|
|
1065
1197
|
markInteracting();
|
|
1066
1198
|
},
|
|
@@ -1074,7 +1206,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
1074
1206
|
const panBy = useCallback(
|
|
1075
1207
|
(dx: number, dy: number) => {
|
|
1076
1208
|
const v = vpRef.current;
|
|
1077
|
-
applyViewport({ x: v.x + dx, y: v.y + dy, zoom: v.zoom });
|
|
1209
|
+
applyViewport({ x: v.x + dx, y: v.y + dy, zoom: v.zoom }, { defer: true });
|
|
1078
1210
|
},
|
|
1079
1211
|
[applyViewport]
|
|
1080
1212
|
);
|
|
@@ -1091,7 +1223,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
1091
1223
|
y: cy - wy * newZoom,
|
|
1092
1224
|
zoom: newZoom,
|
|
1093
1225
|
};
|
|
1094
|
-
applyViewport(next);
|
|
1226
|
+
applyViewport(next, { defer: true });
|
|
1095
1227
|
},
|
|
1096
1228
|
[applyViewport]
|
|
1097
1229
|
);
|
|
@@ -2062,6 +2194,7 @@ export function DCArtboard({
|
|
|
2062
2194
|
print?: ArtboardPrintProp;
|
|
2063
2195
|
children: ReactNode;
|
|
2064
2196
|
}) {
|
|
2197
|
+
countRender('artboardRenders');
|
|
2065
2198
|
const ctx = useWorldContext();
|
|
2066
2199
|
const _controller = useViewportControllerContext();
|
|
2067
2200
|
const toolMode = useToolModeOptional();
|
|
@@ -2105,6 +2238,8 @@ export function DCArtboard({
|
|
|
2105
2238
|
rectFor: (rid) => (ctx ? ctx.rectFor(rid) : null),
|
|
2106
2239
|
allRects: ctx?.artboards ?? [],
|
|
2107
2240
|
viewport: ctx?.viewport ?? null,
|
|
2241
|
+
// Event-time zoom (publishing is settle-only — see applyViewport's `defer`).
|
|
2242
|
+
liveZoom: () => getLiveViewport()?.zoom ?? ctx?.viewport?.zoom ?? 1,
|
|
2108
2243
|
// Cloud Phase 25 C2 — a read-only canvas keeps the Move tool for
|
|
2109
2244
|
// selection, but artboards don't drag (editing is absent, not hidden).
|
|
2110
2245
|
enabled: !!ctx && (toolMode?.tool ?? 'move') === 'move' && !isReadOnlyCanvas(),
|
|
@@ -3308,6 +3443,11 @@ export function DCMiniMap() {
|
|
|
3308
3443
|
active: false,
|
|
3309
3444
|
pointerId: -1,
|
|
3310
3445
|
});
|
|
3446
|
+
// Live, not published: the minimap indicator is one of the two things that
|
|
3447
|
+
// must follow the gesture frame by frame (the other is the zoom readout).
|
|
3448
|
+
// Everything else in the tree deliberately sits still until settle.
|
|
3449
|
+
// Called before the early returns below — hooks cannot sit behind a branch.
|
|
3450
|
+
const vp = useLiveViewport();
|
|
3311
3451
|
|
|
3312
3452
|
if (!world || !controller) return null;
|
|
3313
3453
|
// Menubar "View ▸ Minimap" toggle + Presentation Mode (which hides ALL
|
|
@@ -3316,7 +3456,6 @@ export function DCMiniMap() {
|
|
|
3316
3456
|
|
|
3317
3457
|
const geometry = computeMiniMapGeometry(world.artboards, MAP_W, MAP_BODY_H);
|
|
3318
3458
|
const host = world.hostRef.current;
|
|
3319
|
-
const vp = controller.viewport;
|
|
3320
3459
|
|
|
3321
3460
|
// Visible-area rect in world coords, then projected into map coords.
|
|
3322
3461
|
let vpRect: { left: number; top: number; w: number; h: number } | null = null;
|
|
@@ -3434,10 +3573,13 @@ export function DCZoomToolbar() {
|
|
|
3434
3573
|
ensureOverlayStyles();
|
|
3435
3574
|
const controller = useViewportControllerContext();
|
|
3436
3575
|
const chrome = useChromeVisibility();
|
|
3576
|
+
// Before the early returns — hooks cannot sit behind a branch. Live, because
|
|
3577
|
+
// a zoom readout that only updates on settle reads as a frozen UI.
|
|
3578
|
+
const liveVp = useLiveViewport();
|
|
3437
3579
|
if (!controller) return null;
|
|
3438
3580
|
// Menubar "View ▸ Zoom controls" toggle + Presentation Mode.
|
|
3439
3581
|
if (chrome && (!chrome.zoom || chrome.present)) return null;
|
|
3440
|
-
const pct = Math.round(
|
|
3582
|
+
const pct = Math.round(liveVp.zoom * 100);
|
|
3441
3583
|
return (
|
|
3442
3584
|
<div className="dc-zoom-tb" role="toolbar" aria-label="Zoom">
|
|
3443
3585
|
<button type="button" onClick={controller.zoomOut} aria-label="Zoom out">
|