feat: enhance designer canvas with multi-view support and model viewer integration
Some checks failed
Deploy Production / deploy (push) Failing after 1m11s
Some checks failed
Deploy Production / deploy (push) Failing after 1m11s
- Added canvasId prop to DesignerCanvas for identifying multiple canvases. - Implemented active view selection (front, top, left, right) in designer page. - Updated DesignerCanvas to maintain aspect ratio and dimensions based on view. - Integrated @google/model-viewer for 3D model rendering on the index page. - Refactored useSlipmatDesigner to manage multiple canvases and their states. - Added LAMESA.glb model file for 3D representation. - Updated package.json and package-lock.json to include @google/model-viewer dependency.
This commit is contained in:
@@ -86,9 +86,11 @@ export const useSlipmatDesigner = () => {
|
||||
const selectedTemplate = ref<SlipmatTemplate>(FALLBACK_TEMPLATE);
|
||||
|
||||
const fabricApi = shallowRef<FabricModule | null>(null);
|
||||
const canvas = shallowRef<FabricCanvas | null>(null);
|
||||
const backgroundCircle = shallowRef<FabricCircle | null>(null);
|
||||
const safeZoneCircle = shallowRef<FabricCircle | null>(null);
|
||||
const canvases = shallowRef<Record<string, FabricCanvas>>({});
|
||||
const activeCanvasId = ref<string>('front');
|
||||
const canvas = computed(() => canvases.value[activeCanvasId.value] || null);
|
||||
const backgroundCircle = shallowRef<Record<string, FabricCircle | null>>({});
|
||||
const safeZoneCircle = shallowRef<Record<string, FabricCircle | null>>({});
|
||||
|
||||
const previewUrl = ref<string | null>(null);
|
||||
const previewBlob = shallowRef<Blob | null>(null);
|
||||
@@ -135,20 +137,32 @@ export const useSlipmatDesigner = () => {
|
||||
if (!currentCanvas) {
|
||||
return;
|
||||
}
|
||||
if (backgroundCircle.value) {
|
||||
backgroundCircle.value.set({ radius: currentCanvas.getWidth() / 2 });
|
||||
const bgCircle = backgroundCircle.value[activeCanvasId.value];
|
||||
const safeCircle = safeZoneCircle.value[activeCanvasId.value];
|
||||
|
||||
if (bgCircle) {
|
||||
const bgRect = bgCircle as any;
|
||||
if (bgRect.set) {
|
||||
bgRect.set({
|
||||
width: currentCanvas.getWidth(),
|
||||
height: currentCanvas.getHeight()
|
||||
});
|
||||
}
|
||||
}
|
||||
if (backgroundCircle.value) {
|
||||
currentCanvas.sendObjectToBack(backgroundCircle.value);
|
||||
if (bgCircle) {
|
||||
currentCanvas.sendObjectToBack(bgCircle);
|
||||
}
|
||||
if (safeZoneCircle.value) {
|
||||
currentCanvas.bringObjectToFront(safeZoneCircle.value);
|
||||
if (safeCircle) {
|
||||
currentCanvas.bringObjectToFront(safeCircle);
|
||||
}
|
||||
currentCanvas.requestRenderAll();
|
||||
};
|
||||
|
||||
const isStaticObject = (object: FabricObject | null) =>
|
||||
object === backgroundCircle.value || object === safeZoneCircle.value;
|
||||
const isStaticObject = (object: FabricObject | null) => {
|
||||
const bgCircle = backgroundCircle.value[activeCanvasId.value];
|
||||
const safeCircle = safeZoneCircle.value[activeCanvasId.value];
|
||||
return object === bgCircle || object === safeCircle;
|
||||
};
|
||||
|
||||
const getSelectedObjects = (): FabricObject[] => {
|
||||
const currentCanvas = canvas.value;
|
||||
@@ -222,30 +236,35 @@ export const useSlipmatDesigner = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const size = currentCanvas.getWidth();
|
||||
const width = currentCanvas.getWidth();
|
||||
const height = currentCanvas.getHeight();
|
||||
const { backgroundColor, safeZoneInches = 0, diameterInches } =
|
||||
selectedTemplate.value;
|
||||
|
||||
const bgCircle =
|
||||
backgroundCircle.value ??
|
||||
new fabric.Circle({
|
||||
left: size / 2,
|
||||
top: size / 2,
|
||||
radius: size / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
const canvasId = activeCanvasId.value;
|
||||
const bgRect =
|
||||
backgroundCircle.value[canvasId] as any ??
|
||||
new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
originX: "left",
|
||||
originY: "top",
|
||||
selectable: false,
|
||||
evented: false,
|
||||
rx: 8,
|
||||
ry: 8,
|
||||
});
|
||||
|
||||
bgCircle.set({ fill: backgroundColor });
|
||||
bgCircle.set({ radius: size / 2 });
|
||||
bgRect.set({ fill: backgroundColor });
|
||||
bgRect.set({ width: width, height: height });
|
||||
|
||||
if (!backgroundCircle.value) {
|
||||
backgroundCircle.value = bgCircle;
|
||||
currentCanvas.add(bgCircle);
|
||||
if (!backgroundCircle.value[canvasId]) {
|
||||
backgroundCircle.value[canvasId] = bgRect as any;
|
||||
currentCanvas.add(bgRect);
|
||||
}
|
||||
currentCanvas.sendObjectToBack(bgCircle);
|
||||
currentCanvas.sendObjectToBack(bgRect);
|
||||
currentCanvas.requestRenderAll();
|
||||
|
||||
const safeRatio = Math.max(
|
||||
@@ -253,16 +272,17 @@ export const useSlipmatDesigner = () => {
|
||||
(diameterInches - safeZoneInches * 2) / diameterInches
|
||||
);
|
||||
|
||||
const safeCircleRadius = (size / 2) * safeRatio;
|
||||
const safeMargin = ((1 - safeRatio) * Math.min(width, height)) / 2;
|
||||
|
||||
const safeCircle =
|
||||
safeZoneCircle.value ??
|
||||
new fabric.Circle({
|
||||
left: size / 2,
|
||||
top: size / 2,
|
||||
radius: safeCircleRadius,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
const safeRect =
|
||||
safeZoneCircle.value[canvasId] as any ??
|
||||
new fabric.Rect({
|
||||
left: safeMargin,
|
||||
top: safeMargin,
|
||||
width: width - safeMargin * 2,
|
||||
height: height - safeMargin * 2,
|
||||
originX: "left",
|
||||
originY: "top",
|
||||
fill: "rgba(0,0,0,0)",
|
||||
stroke: "#4b5563",
|
||||
strokeDashArray: [8, 8],
|
||||
@@ -270,36 +290,52 @@ export const useSlipmatDesigner = () => {
|
||||
selectable: false,
|
||||
evented: false,
|
||||
hoverCursor: "default",
|
||||
rx: 8,
|
||||
ry: 8,
|
||||
});
|
||||
|
||||
safeCircle.set({ radius: safeCircleRadius });
|
||||
safeRect.set({
|
||||
left: safeMargin,
|
||||
top: safeMargin,
|
||||
width: width - safeMargin * 2,
|
||||
height: height - safeMargin * 2
|
||||
});
|
||||
|
||||
if (!safeZoneCircle.value) {
|
||||
safeZoneCircle.value = safeCircle;
|
||||
currentCanvas.add(safeCircle);
|
||||
if (!safeZoneCircle.value[canvasId]) {
|
||||
safeZoneCircle.value[canvasId] = safeRect as any;
|
||||
currentCanvas.add(safeRect);
|
||||
}
|
||||
|
||||
maintainStaticLayerOrder();
|
||||
currentCanvas.requestRenderAll();
|
||||
|
||||
currentCanvas.clipPath = new fabric.Circle({
|
||||
left: size / 2,
|
||||
top: size / 2,
|
||||
radius: size / 2,
|
||||
originX: "center",
|
||||
originY: "center",
|
||||
currentCanvas.clipPath = new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
originX: "left",
|
||||
originY: "top",
|
||||
absolutePositioned: true,
|
||||
rx: 8,
|
||||
ry: 8,
|
||||
});
|
||||
|
||||
currentCanvas.renderAll();
|
||||
schedulePreviewRefresh();
|
||||
};
|
||||
|
||||
const registerCanvas = ({ canvas: instance, fabric }: CanvasReadyPayload) => {
|
||||
const registerCanvas = ({ canvas: instance, fabric, canvasId = 'front' }: CanvasReadyPayload & { canvasId?: string }) => {
|
||||
fabricApi.value = fabric;
|
||||
canvas.value = instance;
|
||||
canvases.value[canvasId] = instance;
|
||||
|
||||
const previousActiveId = activeCanvasId.value;
|
||||
// Temporarily set this canvas as active to initialize it
|
||||
activeCanvasId.value = canvasId;
|
||||
|
||||
instance.setDimensions({ width: DISPLAY_SIZE, height: DISPLAY_SIZE });
|
||||
const canvasWidth = DISPLAY_SIZE;
|
||||
const canvasHeight = Math.round(DISPLAY_SIZE * 0.67); // 3:2 aspect ratio for table
|
||||
instance.setDimensions({ width: canvasWidth, height: canvasHeight });
|
||||
instance.preserveObjectStacking = true;
|
||||
|
||||
const refreshEvents = [
|
||||
@@ -352,18 +388,41 @@ export const useSlipmatDesigner = () => {
|
||||
updateSelectedStyleState();
|
||||
setZoom(zoomLevel.value);
|
||||
schedulePreviewRefresh();
|
||||
|
||||
// Restore the active canvas to 'front' if this wasn't the front canvas
|
||||
if (canvasId !== 'front' && previousActiveId) {
|
||||
activeCanvasId.value = previousActiveId;
|
||||
} else if (canvasId === 'front') {
|
||||
// Keep 'front' as active
|
||||
activeCanvasId.value = 'front';
|
||||
}
|
||||
};
|
||||
|
||||
const unregisterCanvas = () => {
|
||||
canvas.value?.dispose();
|
||||
canvas.value = null;
|
||||
fabricApi.value = null;
|
||||
backgroundCircle.value = null;
|
||||
safeZoneCircle.value = null;
|
||||
activeFillColor.value = null;
|
||||
activeStrokeColor.value = null;
|
||||
hasStyleableSelection.value = false;
|
||||
zoomLevel.value = 1;
|
||||
const unregisterCanvas = (canvasId?: string) => {
|
||||
if (canvasId && canvases.value[canvasId]) {
|
||||
canvases.value[canvasId]?.dispose();
|
||||
delete canvases.value[canvasId];
|
||||
delete backgroundCircle.value[canvasId];
|
||||
delete safeZoneCircle.value[canvasId];
|
||||
} else {
|
||||
// Unregister all
|
||||
Object.values(canvases.value).forEach(c => c?.dispose());
|
||||
canvases.value = {};
|
||||
backgroundCircle.value = {};
|
||||
safeZoneCircle.value = {};
|
||||
fabricApi.value = null;
|
||||
activeFillColor.value = null;
|
||||
activeStrokeColor.value = null;
|
||||
hasStyleableSelection.value = false;
|
||||
zoomLevel.value = 1;
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveCanvas = (canvasId: string) => {
|
||||
if (canvases.value[canvasId]) {
|
||||
activeCanvasId.value = canvasId;
|
||||
updateSelectedStyleState();
|
||||
}
|
||||
};
|
||||
|
||||
const centerPoint = () => {
|
||||
@@ -795,6 +854,7 @@ export const useSlipmatDesigner = () => {
|
||||
maxZoom: MAX_ZOOM,
|
||||
registerCanvas,
|
||||
unregisterCanvas,
|
||||
setActiveCanvas,
|
||||
addTextbox,
|
||||
addShape,
|
||||
addImageFromFile,
|
||||
|
||||
Reference in New Issue
Block a user